Data Analyst Project : จะซื้อบ้านสักหลัง ต้องรู้อะไรบ้าง?¶

สวัสดีครับทุกท่านที่กำลังอ่านอยู่ 😀

สำหรับ Project นี้ผมจะสมมุติบทบาทตัวเองว่ากำลังทำงาน Data Analyst ในบริษัทบริหารสินทรัพย์แห่งหนึ่ง โดยธุรกิจหลักของบริษัทนี้คือการซื้อหนี้เสียแบบมีหลักประกันจากสถาบันการเงินต่างๆ โดยหลักประกันในทีนี้คืออสังหาริมทรัพย์

โดยทางเลือกที่ทางบริษัทจะมอบให้แก่ลูกหนี้ที่ผิดชำระมีสองทางเลือกได้แก่

  • เจรจาประนอมนี้ ให้ลูกหนี้ที่ค้างชำระมาชำระหนี้กับเราแทนเพื่อปิดบูโร
  • หากกรณีที่เจรจาไม่ได้ อาจจะต้องยึดหลักประกันมาขายทอดตลาดแทน

โดย Project ที่ผมกำลังโฟกัสอยู่นี้เป็นเรื่องของการทำความเข้าใจตลาดอสังหาริมทรัพย์ รวมไปถึงหา Insight ต่างๆ จากข้อมูลดิบที่มีอยู่ โดยเฉพาะพฤติกรรมของผู้ซื้อบ้านเป็นหลัก

เพราะการเข้าใจพฤติกรรมผู้ซื้อบ้าน และการทำความเข้าใจแนวโน้มต่างๆ ของโครงการอสังหาริมทรัพย์ในท้องที่ รวมไปถึง Factor ต่างๆ ที่มีผลกับราคาบ้าน เป็นอะไรที่สำคัญอย่างยิ่งสำหรับธุรกิจ

เนื่องจากว่าถ้าเราได้หลักประกันในราคาที่แพงเกินไป จะส่งผลต่อกำไรของบริษัทได้

โดย Dataset คราวนี้ที่ผมนำมาใช้

ผมนำมาจาก Kaggle ตาม Link นี้เลยครับ

https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques/data

เพื่อไม่ให้เป็นการเสียเวลา มาเริ่มกันเลย !

Part 1 : จัดการกับข้อมูล¶

ขั้นตอนแรกในการทำงานกับข้อมูลคือการจัดการกับข้อมูล ไล่ตั้งแต่

  • การทำความเข้าใจ Feature ต่างๆ
  • การ Clean ข้อมูลด้วย กำจัดตัวแปรที่ซ้ำ ตัวแปรที่หาย

โดยเครื่องมือยอดฮิตสำหรับงานนี้จะเป็นใครไปไม่ได้นอกจาก Pandas

In [1]:
import pandas as pd
In [2]:
df=pd.read_csv('/content/drive/MyDrive/EDA House Price with Python/train.csv')
df.head()
Out[2]:
Id MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape LandContour Utilities ... PoolArea PoolQC Fence MiscFeature MiscVal MoSold YrSold SaleType SaleCondition SalePrice
0 1 60 RL 65.0 8450 Pave NaN Reg Lvl AllPub ... 0 NaN NaN NaN 0 2 2008 WD Normal 208500
1 2 20 RL 80.0 9600 Pave NaN Reg Lvl AllPub ... 0 NaN NaN NaN 0 5 2007 WD Normal 181500
2 3 60 RL 68.0 11250 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 9 2008 WD Normal 223500
3 4 70 RL 60.0 9550 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 2 2006 WD Abnorml 140000
4 5 60 RL 84.0 14260 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 12 2008 WD Normal 250000

5 rows × 81 columns

ลองพิมพ์ df.info() สักหน่อยเพื่อดูคร่าวๆว่าตัวแปรต่างๆ ถูกเก็บไหนรูปแบบใด

In [3]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1460 entries, 0 to 1459
Data columns (total 81 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   Id             1460 non-null   int64  
 1   MSSubClass     1460 non-null   int64  
 2   MSZoning       1460 non-null   object 
 3   LotFrontage    1201 non-null   float64
 4   LotArea        1460 non-null   int64  
 5   Street         1460 non-null   object 
 6   Alley          91 non-null     object 
 7   LotShape       1460 non-null   object 
 8   LandContour    1460 non-null   object 
 9   Utilities      1460 non-null   object 
 10  LotConfig      1460 non-null   object 
 11  LandSlope      1460 non-null   object 
 12  Neighborhood   1460 non-null   object 
 13  Condition1     1460 non-null   object 
 14  Condition2     1460 non-null   object 
 15  BldgType       1460 non-null   object 
 16  HouseStyle     1460 non-null   object 
 17  OverallQual    1460 non-null   int64  
 18  OverallCond    1460 non-null   int64  
 19  YearBuilt      1460 non-null   int64  
 20  YearRemodAdd   1460 non-null   int64  
 21  RoofStyle      1460 non-null   object 
 22  RoofMatl       1460 non-null   object 
 23  Exterior1st    1460 non-null   object 
 24  Exterior2nd    1460 non-null   object 
 25  MasVnrType     1452 non-null   object 
 26  MasVnrArea     1452 non-null   float64
 27  ExterQual      1460 non-null   object 
 28  ExterCond      1460 non-null   object 
 29  Foundation     1460 non-null   object 
 30  BsmtQual       1423 non-null   object 
 31  BsmtCond       1423 non-null   object 
 32  BsmtExposure   1422 non-null   object 
 33  BsmtFinType1   1423 non-null   object 
 34  BsmtFinSF1     1460 non-null   int64  
 35  BsmtFinType2   1422 non-null   object 
 36  BsmtFinSF2     1460 non-null   int64  
 37  BsmtUnfSF      1460 non-null   int64  
 38  TotalBsmtSF    1460 non-null   int64  
 39  Heating        1460 non-null   object 
 40  HeatingQC      1460 non-null   object 
 41  CentralAir     1460 non-null   object 
 42  Electrical     1459 non-null   object 
 43  1stFlrSF       1460 non-null   int64  
 44  2ndFlrSF       1460 non-null   int64  
 45  LowQualFinSF   1460 non-null   int64  
 46  GrLivArea      1460 non-null   int64  
 47  BsmtFullBath   1460 non-null   int64  
 48  BsmtHalfBath   1460 non-null   int64  
 49  FullBath       1460 non-null   int64  
 50  HalfBath       1460 non-null   int64  
 51  BedroomAbvGr   1460 non-null   int64  
 52  KitchenAbvGr   1460 non-null   int64  
 53  KitchenQual    1460 non-null   object 
 54  TotRmsAbvGrd   1460 non-null   int64  
 55  Functional     1460 non-null   object 
 56  Fireplaces     1460 non-null   int64  
 57  FireplaceQu    770 non-null    object 
 58  GarageType     1379 non-null   object 
 59  GarageYrBlt    1379 non-null   float64
 60  GarageFinish   1379 non-null   object 
 61  GarageCars     1460 non-null   int64  
 62  GarageArea     1460 non-null   int64  
 63  GarageQual     1379 non-null   object 
 64  GarageCond     1379 non-null   object 
 65  PavedDrive     1460 non-null   object 
 66  WoodDeckSF     1460 non-null   int64  
 67  OpenPorchSF    1460 non-null   int64  
 68  EnclosedPorch  1460 non-null   int64  
 69  3SsnPorch      1460 non-null   int64  
 70  ScreenPorch    1460 non-null   int64  
 71  PoolArea       1460 non-null   int64  
 72  PoolQC         7 non-null      object 
 73  Fence          281 non-null    object 
 74  MiscFeature    54 non-null     object 
 75  MiscVal        1460 non-null   int64  
 76  MoSold         1460 non-null   int64  
 77  YrSold         1460 non-null   int64  
 78  SaleType       1460 non-null   object 
 79  SaleCondition  1460 non-null   object 
 80  SalePrice      1460 non-null   int64  
dtypes: float64(3), int64(35), object(43)
memory usage: 924.0+ KB

โห มีตั้ง 80 Column อ่านยากเกิน ขอสลับตารางนิดนึงด้วยการ Transpose จากนั้นใช้คำสั่ง describe เพื่อดูค่าสถิติพื้นฐาน

In [4]:
df.describe().T
Out[4]:
count mean std min 25% 50% 75% max
Id 1460.0 730.500000 421.610009 1.0 365.75 730.5 1095.25 1460.0
MSSubClass 1460.0 56.897260 42.300571 20.0 20.00 50.0 70.00 190.0
LotFrontage 1201.0 70.049958 24.284752 21.0 59.00 69.0 80.00 313.0
LotArea 1460.0 10516.828082 9981.264932 1300.0 7553.50 9478.5 11601.50 215245.0
OverallQual 1460.0 6.099315 1.382997 1.0 5.00 6.0 7.00 10.0
OverallCond 1460.0 5.575342 1.112799 1.0 5.00 5.0 6.00 9.0
YearBuilt 1460.0 1971.267808 30.202904 1872.0 1954.00 1973.0 2000.00 2010.0
YearRemodAdd 1460.0 1984.865753 20.645407 1950.0 1967.00 1994.0 2004.00 2010.0
MasVnrArea 1452.0 103.685262 181.066207 0.0 0.00 0.0 166.00 1600.0
BsmtFinSF1 1460.0 443.639726 456.098091 0.0 0.00 383.5 712.25 5644.0
BsmtFinSF2 1460.0 46.549315 161.319273 0.0 0.00 0.0 0.00 1474.0
BsmtUnfSF 1460.0 567.240411 441.866955 0.0 223.00 477.5 808.00 2336.0
TotalBsmtSF 1460.0 1057.429452 438.705324 0.0 795.75 991.5 1298.25 6110.0
1stFlrSF 1460.0 1162.626712 386.587738 334.0 882.00 1087.0 1391.25 4692.0
2ndFlrSF 1460.0 346.992466 436.528436 0.0 0.00 0.0 728.00 2065.0
LowQualFinSF 1460.0 5.844521 48.623081 0.0 0.00 0.0 0.00 572.0
GrLivArea 1460.0 1515.463699 525.480383 334.0 1129.50 1464.0 1776.75 5642.0
BsmtFullBath 1460.0 0.425342 0.518911 0.0 0.00 0.0 1.00 3.0
BsmtHalfBath 1460.0 0.057534 0.238753 0.0 0.00 0.0 0.00 2.0
FullBath 1460.0 1.565068 0.550916 0.0 1.00 2.0 2.00 3.0
HalfBath 1460.0 0.382877 0.502885 0.0 0.00 0.0 1.00 2.0
BedroomAbvGr 1460.0 2.866438 0.815778 0.0 2.00 3.0 3.00 8.0
KitchenAbvGr 1460.0 1.046575 0.220338 0.0 1.00 1.0 1.00 3.0
TotRmsAbvGrd 1460.0 6.517808 1.625393 2.0 5.00 6.0 7.00 14.0
Fireplaces 1460.0 0.613014 0.644666 0.0 0.00 1.0 1.00 3.0
GarageYrBlt 1379.0 1978.506164 24.689725 1900.0 1961.00 1980.0 2002.00 2010.0
GarageCars 1460.0 1.767123 0.747315 0.0 1.00 2.0 2.00 4.0
GarageArea 1460.0 472.980137 213.804841 0.0 334.50 480.0 576.00 1418.0
WoodDeckSF 1460.0 94.244521 125.338794 0.0 0.00 0.0 168.00 857.0
OpenPorchSF 1460.0 46.660274 66.256028 0.0 0.00 25.0 68.00 547.0
EnclosedPorch 1460.0 21.954110 61.119149 0.0 0.00 0.0 0.00 552.0
3SsnPorch 1460.0 3.409589 29.317331 0.0 0.00 0.0 0.00 508.0
ScreenPorch 1460.0 15.060959 55.757415 0.0 0.00 0.0 0.00 480.0
PoolArea 1460.0 2.758904 40.177307 0.0 0.00 0.0 0.00 738.0
MiscVal 1460.0 43.489041 496.123024 0.0 0.00 0.0 0.00 15500.0
MoSold 1460.0 6.321918 2.703626 1.0 5.00 6.0 8.00 12.0
YrSold 1460.0 2007.815753 1.328095 2006.0 2007.00 2008.0 2009.00 2010.0
SalePrice 1460.0 180921.195890 79442.502883 34900.0 129975.00 163000.0 214000.00 755000.0

ลองตรวจสอบค่าในที่มีความเฉพาะในแต่ละ Columns ดูดีกว่าว่าในนั้นมตัวแปรอะไรเก็บไว้อยู่บ้าน โดยเราจะเริ่มจาก

  1. แยกตัวแปรที่เป็นตัวแปรเชิงปริมาณออกมาก่อน โดยตัวแปรกลุ่มนี้มักจะถูกเก็บในรูป object
  2. ไล่ Print แต่ละ Columns ของ DataFrame ออกมาสำรวจสักหน่อย

โดยผลลัพธ์ที่ได้จะอยู่ด้านล่าง

In [5]:
# Get only columns of object type
object_cols = df.select_dtypes(include=['object']).columns

# Iterate over object columns and print unique values
for col in object_cols:
    print(f"Column '{col}' unique values: ", df[col].unique())
Column 'MSZoning' unique values:  ['RL' 'RM' 'C (all)' 'FV' 'RH']
Column 'Street' unique values:  ['Pave' 'Grvl']
Column 'Alley' unique values:  [nan 'Grvl' 'Pave']
Column 'LotShape' unique values:  ['Reg' 'IR1' 'IR2' 'IR3']
Column 'LandContour' unique values:  ['Lvl' 'Bnk' 'Low' 'HLS']
Column 'Utilities' unique values:  ['AllPub' 'NoSeWa']
Column 'LotConfig' unique values:  ['Inside' 'FR2' 'Corner' 'CulDSac' 'FR3']
Column 'LandSlope' unique values:  ['Gtl' 'Mod' 'Sev']
Column 'Neighborhood' unique values:  ['CollgCr' 'Veenker' 'Crawfor' 'NoRidge' 'Mitchel' 'Somerst' 'NWAmes'
 'OldTown' 'BrkSide' 'Sawyer' 'NridgHt' 'NAmes' 'SawyerW' 'IDOTRR'
 'MeadowV' 'Edwards' 'Timber' 'Gilbert' 'StoneBr' 'ClearCr' 'NPkVill'
 'Blmngtn' 'BrDale' 'SWISU' 'Blueste']
Column 'Condition1' unique values:  ['Norm' 'Feedr' 'PosN' 'Artery' 'RRAe' 'RRNn' 'RRAn' 'PosA' 'RRNe']
Column 'Condition2' unique values:  ['Norm' 'Artery' 'RRNn' 'Feedr' 'PosN' 'PosA' 'RRAn' 'RRAe']
Column 'BldgType' unique values:  ['1Fam' '2fmCon' 'Duplex' 'TwnhsE' 'Twnhs']
Column 'HouseStyle' unique values:  ['2Story' '1Story' '1.5Fin' '1.5Unf' 'SFoyer' 'SLvl' '2.5Unf' '2.5Fin']
Column 'RoofStyle' unique values:  ['Gable' 'Hip' 'Gambrel' 'Mansard' 'Flat' 'Shed']
Column 'RoofMatl' unique values:  ['CompShg' 'WdShngl' 'Metal' 'WdShake' 'Membran' 'Tar&Grv' 'Roll'
 'ClyTile']
Column 'Exterior1st' unique values:  ['VinylSd' 'MetalSd' 'Wd Sdng' 'HdBoard' 'BrkFace' 'WdShing' 'CemntBd'
 'Plywood' 'AsbShng' 'Stucco' 'BrkComm' 'AsphShn' 'Stone' 'ImStucc'
 'CBlock']
Column 'Exterior2nd' unique values:  ['VinylSd' 'MetalSd' 'Wd Shng' 'HdBoard' 'Plywood' 'Wd Sdng' 'CmentBd'
 'BrkFace' 'Stucco' 'AsbShng' 'Brk Cmn' 'ImStucc' 'AsphShn' 'Stone'
 'Other' 'CBlock']
Column 'MasVnrType' unique values:  ['BrkFace' 'None' 'Stone' 'BrkCmn' nan]
Column 'ExterQual' unique values:  ['Gd' 'TA' 'Ex' 'Fa']
Column 'ExterCond' unique values:  ['TA' 'Gd' 'Fa' 'Po' 'Ex']
Column 'Foundation' unique values:  ['PConc' 'CBlock' 'BrkTil' 'Wood' 'Slab' 'Stone']
Column 'BsmtQual' unique values:  ['Gd' 'TA' 'Ex' nan 'Fa']
Column 'BsmtCond' unique values:  ['TA' 'Gd' nan 'Fa' 'Po']
Column 'BsmtExposure' unique values:  ['No' 'Gd' 'Mn' 'Av' nan]
Column 'BsmtFinType1' unique values:  ['GLQ' 'ALQ' 'Unf' 'Rec' 'BLQ' nan 'LwQ']
Column 'BsmtFinType2' unique values:  ['Unf' 'BLQ' nan 'ALQ' 'Rec' 'LwQ' 'GLQ']
Column 'Heating' unique values:  ['GasA' 'GasW' 'Grav' 'Wall' 'OthW' 'Floor']
Column 'HeatingQC' unique values:  ['Ex' 'Gd' 'TA' 'Fa' 'Po']
Column 'CentralAir' unique values:  ['Y' 'N']
Column 'Electrical' unique values:  ['SBrkr' 'FuseF' 'FuseA' 'FuseP' 'Mix' nan]
Column 'KitchenQual' unique values:  ['Gd' 'TA' 'Ex' 'Fa']
Column 'Functional' unique values:  ['Typ' 'Min1' 'Maj1' 'Min2' 'Mod' 'Maj2' 'Sev']
Column 'FireplaceQu' unique values:  [nan 'TA' 'Gd' 'Fa' 'Ex' 'Po']
Column 'GarageType' unique values:  ['Attchd' 'Detchd' 'BuiltIn' 'CarPort' nan 'Basment' '2Types']
Column 'GarageFinish' unique values:  ['RFn' 'Unf' 'Fin' nan]
Column 'GarageQual' unique values:  ['TA' 'Fa' 'Gd' nan 'Ex' 'Po']
Column 'GarageCond' unique values:  ['TA' 'Fa' nan 'Gd' 'Po' 'Ex']
Column 'PavedDrive' unique values:  ['Y' 'N' 'P']
Column 'PoolQC' unique values:  [nan 'Ex' 'Fa' 'Gd']
Column 'Fence' unique values:  [nan 'MnPrv' 'GdWo' 'GdPrv' 'MnWw']
Column 'MiscFeature' unique values:  [nan 'Shed' 'Gar2' 'Othr' 'TenC']
Column 'SaleType' unique values:  ['WD' 'New' 'COD' 'ConLD' 'ConLI' 'CWD' 'ConLw' 'Con' 'Oth']
Column 'SaleCondition' unique values:  ['Normal' 'Abnorml' 'Partial' 'AdjLand' 'Alloca' 'Family']

แย่ละครับ บอกเลยว่าอ่านไม่รู้เรื่องเลย มันให้มาเป็นสัญลักษณ์อะไรก็ไม่รู้ แน่นอนว่าเจอแบบนี้ต้องไปดู Data Description ว่ามันให้อะไรมา

มาถึงตรงนี้ผมตัดสินใจแล้วว่าจะเปลี่ยนไอ้ตัวแปรรหัสพวกนี้ไปเป็นคำอธิบายไปเลย เพื่อความเข้าใจที่ง่ายขึ้น(ของตัวผมเอง)

บรรทัดนี้ซ้อมก่อนครับ กลัวทำพลาด ข้ามไปได้เลย

In [6]:
import re
import pandas as pd

def extract_values_to_dataframe(content):

    # ใช้ Regex ในการช่วยอ่าน Pattern
    pattern = r'(?P<Label>^[^:]+):\s*(?P<Description>[^\n]+)(?P<Values>(?:\n\t\d+\t[^\n]+)+)'

    # เอาเฉพาะตัวที่มัน Match กับ Pattern
    matches = re.finditer(pattern, content, re.MULTILINE)

    # สร้าง dictionary มาเก็บไว้
    data_dict = {}

    for match in matches:
        label = match.group("Label").strip()
        values_section = match.group("Values")
        value_pairs = re.findall(r'\t(\d+)\t([^\n]+)', values_section)

        # ยัดข้อมูลที่ตัดมาได้ไปไว้ใน Dict
        data_dict[label] = {int(key): value for key, value in value_pairs}

    # ยัดข้อมูลใน Dict ไปไว้ใน DataFrame อีกที
    result_df = pd.DataFrame([data_dict]).T
    result_df.columns = ["Enumerated Values"]

    return result_df

# ทดสอบ
mock_content = """
Color: Describes the color
	1	Red
	2	Blue
	3	Green

Size: Indicates size
	1	Small
	2	Medium
	3	Large
"""

result_df = extract_values_to_dataframe(mock_content)
print(result_df)

mapping_dict = result_df["Enumerated Values"].to_dict()

b = {
    "Color": [1, 2, 3, 1, 3],
    "Size": [2, 3, 1, 2, 3]
}
a = pd.DataFrame(b)

# ลองเปลี่ยน
a['Color'] = a['Color'].map(mapping_dict['Color'])
a['Size'] = a['Size'].map(mapping_dict['Size'])

print(a)
                           Enumerated Values
Color      {1: 'Red', 2: 'Blue', 3: 'Green'}
Size   {1: 'Small', 2: 'Medium', 3: 'Large'}
   Color    Size
0    Red  Medium
1   Blue   Large
2  Green   Small
3    Red  Medium
4  Green   Large

ทีนี้ลองมาดูของจริงกันบ้าง (อันนี้เปลี่ยนวิธีอ่านนิดนึงครับ สังเกตุว่าใน txt.file มันใช้ Tap ขั้น) และจุดที่น่าสังเกตุคือตัวแปรที่เป็น Categorial มันจะมี Subkey ด้านใน ในขณะที่ตัวแปรประเภท Numerical มันจะมีคำบรรยายแค่บรรทัดเดียว

ตัวอย่าง

MSZoning: Identifies the general zoning classification of the sale.

   A        Agriculture
   C        Commercial
   FV   Floating Village Residential
   I        Industrial
   RH   Residential High Density
   RL   Residential Low Density
   RP   Residential Low Density Park
   RM   Residential Medium Density

LotFrontage: Linear feet of street connected to property

LotArea: Lot size in square feet

Street: Type of road access to property

   Grvl Gravel
   Pave Paved

ทีนี้มาพูดถึงวิธีการตัดกันบ้าง เราจะใช้ Dictionary 2 ชุดคือ temp_dict และ property_dict โดยภาพที่วางไว้คือเราจะตัดค่าต่างๆ ยังใส่ temp_dict ก่อน จากนั้นถ้าเจอ key ใหม่(วนครบทุก sub_key ใน key หลักแล้ว) เราจะยัดก้อนที่เสร็จแล้วใส่ property_dict ทำแบบนี้ไปเรื่อยๆ จนเสร็จ

คำถามคือตอนนี้เรามองหาอะไร ? จาก Pattern ที่ให้มามันจะมี 2 Pattern ที่ไว้แยก Key กับ sub_keys โดย

Key:Description

sub_keys: Value

ทีนี้มาดูแนวคิดการตัดกันต่อ

1.ขั้นตอนคือเรามองหาเครื่องหมาย : เพราะ Pattern มันจะเป็น Key:Description

2.วิธีการเช็คว่ามันเป็น Sub_key จริงไหมทำได้โดยการใช้ len(stripped_line.split("\t")) == 2: เพราะถ้าเรา split ตัวที่ขั้นด้วย Tap มันจะต้องแยกออกมาได้ 2 ตัว

In [7]:
def extract_enumerations_from_file(file_path):
    # สร้าง temp_dict ขึ้นมา
    temp_dict = {}


    # สร้าง current_key กับ current_desc ขึ้นมาแบบเปล่าๆ ก่อน
    current_key = None
    current_desc = None

    with open(file_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()

        for line in lines:
            stripped_line = line.strip()

            # เงื่อนไขนี้เช็คว่าบรรทัดที่มี colon(;) เป็นบรรทัดหลักใช่หรือไม่ ?
            if ':' in stripped_line:
                # ถ้าผ่านเงื่อนไขข้างบนมา เราก็ต้องมาเช็คต่อว่ามี Sub_keys ไหม ถ้ามีเราถึงจะเก็บเค้าไว้ใน Property_dict ถ้าไม่มีก็โยนทิ้ง ไม่บันทึก
                if current_key and temp_dict[current_key]["sub_keys"]:
                    property_dict[current_key] = temp_dict[current_key]
                # คือถ้าผ่านเงื่อนไขนี้ได้ แปลว่ายังไงมันก็ต้องเก็บ current_key, current_desc ไว้ก่อน
                #โดยเก็บไว้ด้วย {"desc": current_desc.strip(), "sub_keys": {}} ว่าง sub_keys ไว้รอเติม
                current_key, current_desc = stripped_line.split(":")
                temp_dict[current_key] = {"desc": current_desc.strip(), "sub_keys": {}}
            # ถ้ามาเงื่อนไขนี้แปลว่ากำลังเช็คว่าบรรทัดที่อ่านอยู่เป็น sub_key ใช่หรือไม่ ? ถ้าใช่เราจะเก็บเค้าไว้ใน Temp Dict
            elif current_key and len(stripped_line.split("\t")) == 2:
                sub_key, value = stripped_line.split("\t")
                temp_dict[current_key]["sub_keys"][sub_key.strip()] = value.strip()

    # อันนี้เผื่อไว้สำหรับ Key สุดท้ายถ้ามันมี sub_keys
    if current_key and temp_dict[current_key]["sub_keys"]:
        property_dict[current_key] = temp_dict[current_key]

    return property_dict

# สร้าง property_dict ขึ้นมา
property_dict = {}



#อันนี้ทำ fucntion สำหรับอ่าน dictionary เฉยๆ
def print_enum_dict(dictionary):
    for key, sub_dict in dictionary.items():
        print(f"{key}:")
        for sub_key, value in sub_dict.items():
            print(f"    {sub_key} -> {value}")
        print()


# ใช้งาน
file_path = '/content/drive/MyDrive/EDA House Price with Python/data_description (1).txt'
enum_dict_2 = extract_enumerations_from_file(file_path)
print_enum_dict(enum_dict_2)
MSSubClass:
    desc -> Identifies the type of dwelling involved in the sale.
    sub_keys -> {'20': '1-STORY 1946 & NEWER ALL STYLES', '30': '1-STORY 1945 & OLDER', '40': '1-STORY W/FINISHED ATTIC ALL AGES', '45': '1-1/2 STORY - UNFINISHED ALL AGES', '50': '1-1/2 STORY FINISHED ALL AGES', '60': '2-STORY 1946 & NEWER', '70': '2-STORY 1945 & OLDER', '75': '2-1/2 STORY ALL AGES', '80': 'SPLIT OR MULTI-LEVEL', '85': 'SPLIT FOYER', '90': 'DUPLEX - ALL STYLES AND AGES', '120': '1-STORY PUD (Planned Unit Development) - 1946 & NEWER', '150': '1-1/2 STORY PUD - ALL AGES', '160': '2-STORY PUD - 1946 & NEWER', '180': 'PUD - MULTILEVEL - INCL SPLIT LEV/FOYER', '190': '2 FAMILY CONVERSION - ALL STYLES AND AGES'}

MSZoning:
    desc -> Identifies the general zoning classification of the sale.
    sub_keys -> {'FV': 'Floating Village Residential', 'RH': 'Residential High Density', 'RL': 'Residential Low Density', 'RP': 'Residential Low Density Park', 'RM': 'Residential Medium Density'}

Street:
    desc -> Type of road access to property
    sub_keys -> {'Grvl': 'Gravel', 'Pave': 'Paved'}

Alley:
    desc -> Type of alley access to property
    sub_keys -> {'Grvl': 'Gravel', 'Pave': 'Paved', 'NA': 'No alley access'}

LotShape:
    desc -> General shape of property
    sub_keys -> {'Reg': 'Regular', 'IR1': 'Slightly irregular', 'IR2': 'Moderately Irregular', 'IR3': 'Irregular'}

LandContour:
    desc -> Flatness of the property
    sub_keys -> {'Lvl': 'Near Flat/Level', 'Bnk': 'Banked - Quick and significant rise from street grade to building', 'HLS': 'Hillside - Significant slope from side to side', 'Low': 'Depression'}

Utilities:
    desc -> Type of utilities available
    sub_keys -> {'AllPub': 'All public Utilities (E,G,W,& S)', 'NoSewr': 'Electricity, Gas, and Water (Septic Tank)', 'NoSeWa': 'Electricity and Gas Only', 'ELO': 'Electricity only'}

LotConfig:
    desc -> Lot configuration
    sub_keys -> {'Inside': 'Inside lot', 'Corner': 'Corner lot', 'CulDSac': 'Cul-de-sac', 'FR2': 'Frontage on 2 sides of property', 'FR3': 'Frontage on 3 sides of property'}

LandSlope:
    desc -> Slope of property
    sub_keys -> {'Gtl': 'Gentle slope', 'Mod': 'Moderate Slope', 'Sev': 'Severe Slope'}

Neighborhood:
    desc -> Physical locations within Ames city limits
    sub_keys -> {'Blmngtn': 'Bloomington Heights', 'Blueste': 'Bluestem', 'BrDale': 'Briardale', 'BrkSide': 'Brookside', 'ClearCr': 'Clear Creek', 'CollgCr': 'College Creek', 'Crawfor': 'Crawford', 'Edwards': 'Edwards', 'Gilbert': 'Gilbert', 'IDOTRR': 'Iowa DOT and Rail Road', 'MeadowV': 'Meadow Village', 'Mitchel': 'Mitchell', 'Names': 'North Ames', 'NoRidge': 'Northridge', 'NPkVill': 'Northpark Villa', 'NridgHt': 'Northridge Heights', 'NWAmes': 'Northwest Ames', 'OldTown': 'Old Town', 'SWISU': 'South & West of Iowa State University', 'Sawyer': 'Sawyer', 'SawyerW': 'Sawyer West', 'Somerst': 'Somerset', 'StoneBr': 'Stone Brook', 'Timber': 'Timberland', 'Veenker': 'Veenker'}

Condition1:
    desc -> Proximity to various conditions
    sub_keys -> {'Artery': 'Adjacent to arterial street', 'Feedr': 'Adjacent to feeder street', 'Norm': 'Normal', 'RRNn': "Within 200' of North-South Railroad", 'RRAn': 'Adjacent to North-South Railroad', 'PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'PosA': 'Adjacent to postive off-site feature', 'RRNe': "Within 200' of East-West Railroad", 'RRAe': 'Adjacent to East-West Railroad'}

Condition2:
    desc -> Proximity to various conditions (if more than one is present)
    sub_keys -> {'Artery': 'Adjacent to arterial street', 'Feedr': 'Adjacent to feeder street', 'Norm': 'Normal', 'RRNn': "Within 200' of North-South Railroad", 'RRAn': 'Adjacent to North-South Railroad', 'PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'PosA': 'Adjacent to postive off-site feature', 'RRNe': "Within 200' of East-West Railroad", 'RRAe': 'Adjacent to East-West Railroad'}

BldgType:
    desc -> Type of dwelling
    sub_keys -> {'1Fam': 'Single-family Detached', '2FmCon': 'Two-family Conversion; originally built as one-family dwelling', 'Duplx': 'Duplex', 'TwnhsE': 'Townhouse End Unit', 'TwnhsI': 'Townhouse Inside Unit'}

HouseStyle:
    desc -> Style of dwelling
    sub_keys -> {'1Story': 'One story'}

1.5Unf	One and one-half story:
    desc -> 2nd level unfinished
    sub_keys -> {'2Story': 'Two story'}

2.5Unf	Two and one-half story:
    desc -> 2nd level unfinished
    sub_keys -> {'SFoyer': 'Split Foyer', 'SLvl': 'Split Level'}

OverallQual:
    desc -> Rates the overall material and finish of the house
    sub_keys -> {'10': 'Very Excellent', '9': 'Excellent', '8': 'Very Good', '7': 'Good', '6': 'Above Average', '5': 'Average', '4': 'Below Average', '3': 'Fair', '2': 'Poor', '1': 'Very Poor'}

OverallCond:
    desc -> Rates the overall condition of the house
    sub_keys -> {'10': 'Very Excellent', '9': 'Excellent', '8': 'Very Good', '7': 'Good', '6': 'Above Average', '5': 'Average', '4': 'Below Average', '3': 'Fair', '2': 'Poor', '1': 'Very Poor'}

RoofStyle:
    desc -> Type of roof
    sub_keys -> {'Flat': 'Flat', 'Gable': 'Gable', 'Gambrel': 'Gabrel (Barn)', 'Hip': 'Hip', 'Mansard': 'Mansard', 'Shed': 'Shed'}

RoofMatl:
    desc -> Roof material
    sub_keys -> {'ClyTile': 'Clay or Tile', 'CompShg': 'Standard (Composite) Shingle', 'Membran': 'Membrane', 'Metal': 'Metal', 'Roll': 'Roll', 'Tar&Grv': 'Gravel & Tar', 'WdShake': 'Wood Shakes', 'WdShngl': 'Wood Shingles'}

Exterior1st:
    desc -> Exterior covering on house
    sub_keys -> {'AsbShng': 'Asbestos Shingles', 'AsphShn': 'Asphalt Shingles', 'BrkComm': 'Brick Common', 'BrkFace': 'Brick Face', 'CBlock': 'Cinder Block', 'CemntBd': 'Cement Board', 'HdBoard': 'Hard Board', 'ImStucc': 'Imitation Stucco', 'MetalSd': 'Metal Siding', 'Other': 'Other', 'Plywood': 'Plywood', 'PreCast': 'PreCast', 'Stone': 'Stone', 'Stucco': 'Stucco', 'VinylSd': 'Vinyl Siding', 'Wd Sdng': 'Wood Siding', 'WdShing': 'Wood Shingles'}

Exterior2nd:
    desc -> Exterior covering on house (if more than one material)
    sub_keys -> {'AsbShng': 'Asbestos Shingles', 'AsphShn': 'Asphalt Shingles', 'BrkComm': 'Brick Common', 'BrkFace': 'Brick Face', 'CBlock': 'Cinder Block', 'CemntBd': 'Cement Board', 'HdBoard': 'Hard Board', 'ImStucc': 'Imitation Stucco', 'MetalSd': 'Metal Siding', 'Other': 'Other', 'Plywood': 'Plywood', 'PreCast': 'PreCast', 'Stone': 'Stone', 'Stucco': 'Stucco', 'VinylSd': 'Vinyl Siding', 'Wd Sdng': 'Wood Siding', 'WdShing': 'Wood Shingles'}

MasVnrType:
    desc -> Masonry veneer type
    sub_keys -> {'BrkCmn': 'Brick Common', 'BrkFace': 'Brick Face', 'CBlock': 'Cinder Block', 'None': 'None', 'Stone': 'Stone'}

ExterQual:
    desc -> Evaluates the quality of the material on the exterior
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'Po': 'Poor'}

ExterCond:
    desc -> Evaluates the present condition of the material on the exterior
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'Po': 'Poor'}

Foundation:
    desc -> Type of foundation
    sub_keys -> {'BrkTil': 'Brick & Tile', 'CBlock': 'Cinder Block', 'PConc': 'Poured Contrete', 'Slab': 'Slab', 'Stone': 'Stone', 'Wood': 'Wood'}

BsmtQual:
    desc -> Evaluates the height of the basement
    sub_keys -> {'Ex': 'Excellent (100+ inches)', 'Gd': 'Good (90-99 inches)', 'TA': 'Typical (80-89 inches)', 'Fa': 'Fair (70-79 inches)', 'Po': 'Poor (<70 inches', 'NA': 'No Basement'}

BsmtCond:
    desc -> Evaluates the general condition of the basement
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical - slight dampness allowed', 'Fa': 'Fair - dampness or some cracking or settling', 'Po': 'Poor - Severe cracking, settling, or wetness', 'NA': 'No Basement'}

BsmtExposure:
    desc -> Refers to walkout or garden level walls
    sub_keys -> {'Gd': 'Good Exposure', 'Av': 'Average Exposure (split levels or foyers typically score average or above)', 'Mn': 'Mimimum Exposure', 'No': 'No Exposure', 'NA': 'No Basement'}

BsmtFinType1:
    desc -> Rating of basement finished area
    sub_keys -> {'GLQ': 'Good Living Quarters', 'ALQ': 'Average Living Quarters', 'BLQ': 'Below Average Living Quarters', 'Rec': 'Average Rec Room', 'LwQ': 'Low Quality', 'Unf': 'Unfinshed', 'NA': 'No Basement'}

BsmtFinType2:
    desc -> Rating of basement finished area (if multiple types)
    sub_keys -> {'GLQ': 'Good Living Quarters', 'ALQ': 'Average Living Quarters', 'BLQ': 'Below Average Living Quarters', 'Rec': 'Average Rec Room', 'LwQ': 'Low Quality', 'Unf': 'Unfinshed', 'NA': 'No Basement'}

Heating:
    desc -> Type of heating
    sub_keys -> {'Floor': 'Floor Furnace', 'GasA': 'Gas forced warm air furnace', 'GasW': 'Gas hot water or steam heat', 'Grav': 'Gravity furnace', 'OthW': 'Hot water or steam heat other than gas', 'Wall': 'Wall furnace'}

HeatingQC:
    desc -> Heating quality and condition
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'Po': 'Poor'}

CentralAir:
    desc -> Central air conditioning
    sub_keys -> {'N': 'No', 'Y': 'Yes'}

Electrical:
    desc -> Electrical system
    sub_keys -> {'SBrkr': 'Standard Circuit Breakers & Romex', 'FuseA': 'Fuse Box over 60 AMP and all Romex wiring (Average)', 'FuseF': '60 AMP Fuse Box and mostly Romex wiring (Fair)', 'FuseP': '60 AMP Fuse Box and mostly knob & tube wiring (poor)', 'Mix': 'Mixed'}

KitchenQual:
    desc -> Kitchen quality
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical/Average', 'Fa': 'Fair', 'Po': 'Poor'}

Functional:
    desc -> Home functionality (Assume typical unless deductions are warranted)
    sub_keys -> {'Typ': 'Typical Functionality', 'Min1': 'Minor Deductions 1', 'Min2': 'Minor Deductions 2', 'Mod': 'Moderate Deductions', 'Maj1': 'Major Deductions 1', 'Maj2': 'Major Deductions 2', 'Sev': 'Severely Damaged', 'Sal': 'Salvage only'}

FireplaceQu:
    desc -> Fireplace quality
    sub_keys -> {'Ex': 'Excellent - Exceptional Masonry Fireplace', 'Gd': 'Good - Masonry Fireplace in main level', 'TA': 'Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement', 'Fa': 'Fair - Prefabricated Fireplace in basement', 'Po': 'Poor - Ben Franklin Stove', 'NA': 'No Fireplace'}

GarageType:
    desc -> Garage location
    sub_keys -> {'2Types': 'More than one type of garage', 'Attchd': 'Attached to home', 'Basment': 'Basement Garage', 'BuiltIn': 'Built-In (Garage part of house - typically has room above garage)', 'CarPort': 'Car Port', 'Detchd': 'Detached from home', 'NA': 'No Garage'}

GarageFinish:
    desc -> Interior finish of the garage
    sub_keys -> {'Fin': 'Finished', 'RFn': 'Rough Finished', 'Unf': 'Unfinished', 'NA': 'No Garage'}

GarageQual:
    desc -> Garage quality
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical/Average', 'Fa': 'Fair', 'Po': 'Poor', 'NA': 'No Garage'}

GarageCond:
    desc -> Garage condition
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Typical/Average', 'Fa': 'Fair', 'Po': 'Poor', 'NA': 'No Garage'}

PavedDrive:
    desc -> Paved driveway
    sub_keys -> {'Y': 'Paved', 'P': 'Partial Pavement', 'N': 'Dirt/Gravel'}

PoolQC:
    desc -> Pool quality
    sub_keys -> {'Ex': 'Excellent', 'Gd': 'Good', 'TA': 'Average/Typical', 'Fa': 'Fair', 'NA': 'No Pool'}

Fence:
    desc -> Fence quality
    sub_keys -> {'GdPrv': 'Good Privacy', 'MnPrv': 'Minimum Privacy', 'GdWo': 'Good Wood', 'MnWw': 'Minimum Wood/Wire', 'NA': 'No Fence'}

MiscFeature:
    desc -> Miscellaneous feature not covered in other categories
    sub_keys -> {'Elev': 'Elevator', 'Gar2': '2nd Garage (if not described in garage section)', 'Othr': 'Other', 'Shed': 'Shed (over 100 SF)', 'TenC': 'Tennis Court', 'NA': 'None'}

SaleType:
    desc -> Type of sale
    sub_keys -> {'WD': 'Warranty Deed - Conventional', 'CWD': 'Warranty Deed - Cash', 'VWD': 'Warranty Deed - VA Loan', 'New': 'Home just constructed and sold', 'COD': 'Court Officer Deed/Estate', 'Con': 'Contract 15% Down payment regular terms', 'ConLw': 'Contract Low Down payment and low interest', 'ConLI': 'Contract Low Interest', 'ConLD': 'Contract Low Down', 'Oth': 'Other'}

SaleCondition:
    desc -> Condition of sale
    sub_keys -> {'Normal': 'Normal Sale', 'Abnorml': 'Abnormal Sale -  trade, foreclosure, short sale', 'AdjLand': 'Adjoining Land Purchase', 'Alloca': 'Allocation - two linked properties with separate deeds, typically condo with a garage unit', 'Family': 'Sale between family members', 'Partial': 'Home was not completed when last assessed (associated with New Homes)'}

เดี๋ยวถึงตรงนี้ขอหยุดขั้นตอนแปลงไว้ตรงนี้ก่อน พอดีลองอ่าน Description แล้วพบว่าตัว NaN ในข้อมูลชุดนี้หมายถึง "ไม่มี"

โดยปกติแล้วข้อมูลที่เป็น Null Value มันเกิดจากการลืมเติม หรือข้อมูลหายไป แต่กับข้อมูลชุดนี้ ซึ่งเกี่ยวกับอสังหาริมทรัพย์ คำว่าไม่มีมันก็คือไม่มี

ถ้ายกตัวอย่างง่ายๆ เช่น PoolQC = NA มันกำลังบอกว่าบ้านหลังนี้ไม่มีสระว่ายน้ำ ไม่ได้แปลว่าลืมเติมแต่อย่างใด

ทั้งนี้การจัดการกับข้อมูลที่หายไป หลักๆ จะต้มยำทำแกงบ่อยๆ ได้ดังนี้

Numerical

  • ใช้ค่าเฉลี่ย *ใช้การ Interpolate สำหรับ Time Series

Categorial

  • ลบทั้งบรรทัดทิ้ง
  • เติมตัวซ้ำมากที่สุดเข้าไปแทน
  • เปลี่ยน Missing Value เป็น Category ใหม่(Data ชุดนี้ทำแบบนี้)

ทั้งนี้ เพื่อให้เห็นภาพมากขึ้น เราจะทำการนับ Missing Value แล้วเอามา Plot เพื่อความชัดเจน

In [8]:
missing = df.isnull().sum()
missing = missing[missing > 0]
missing.sort_values(inplace=True)
missing.plot.bar()
Out[8]:
<Axes: >

โอเค เดี๋ยวแยก Quantitative กับ Qualitative ดีกว่า

In [9]:
quantitative = df.select_dtypes(include=(['int64', 'float64']))
quantitative_missing = quantitative.isnull().sum()

quantitative_missing = quantitative_missing[quantitative_missing > 0]
quantitative_missing.sort_values(inplace=True)
quantitative_missing.plot.bar()
Out[9]:
<Axes: >

เราต้องมาเช็คอีกทีว่า GarageYrBlt กับ MasVnrArea ที่หายไปมันเท่ากับ GarageType,MasVnrType หรือไม่ ถ้ามันเท่ากันแปลว่าช่องที่หายไป ควรจะ

  • เติมเลข 0 ในช่องที่เป็นตัวเลข (MasVnrArea , GarageYrBlt)
  • เติม 'NA' ในช่องที่เป็นตัวแปร Categorial (GarageType,MasVnrType)
In [10]:
print(df['GarageType'].isnull().sum() == df['GarageYrBlt'].isnull().sum())
print(df['MasVnrArea'].isnull().sum() == df['MasVnrType'].isnull().sum())
True
True
In [11]:
df['GarageYrBlt']=df['GarageYrBlt'].fillna(0)
df['MasVnrArea']=df['MasVnrArea'].fillna(0)
df['LotFrontage']=df['LotFrontage'].fillna(0)

เช็คอีกทีว่ามีตัวแปรไหนหายไปไหม ?

In [12]:
missing_value_found = False

for col in quantitative:
    if df[col].isnull().sum() > 0:
        print(col)
        missing_value_found = True

if not missing_value_found:
    print("No Missing Value")
No Missing Value

ทำแบบเดิมกับตัวแปร Qualitative อีกรอบ

In [13]:
qualitative = df.select_dtypes(include=['object'])

qualitative_missing = qualitative.isnull().sum()

qualitative_missing = qualitative_missing[qualitative_missing > 0]
qualitative_missing.sort_values(inplace=True)
qualitative_missing.plot.bar()
Out[13]:
<Axes: >

ยัด NA เข้าไปในทุกช่องว่าง แต่สำหรับ MasVnrType มันจะมีปัญหานิดหน่อยเพราะมีทั้ง NA และ None เลยต้องเปลี่ยนอีกที

In [14]:
for col in qualitative:
  df[col]=df[col].fillna('NA')

df.head()

df['MasVnrType']=df['MasVnrType'].replace(['NA'],'None')

df['MasVnrType'].unique()
Out[14]:
array(['BrkFace', 'None', 'Stone', 'BrkCmn'], dtype=object)

กลับมาที่ Dictionary ของเรา ด้วยความที่ชุดข้อมูลที่เราตัดมามันเป็น Nested Dictionary และเอาไปใช้ยาก ดังนั้นการตัดให้มันเป็นบรรทัดเดียวแบบ Dictionary ทั่วไปที่บันทึก Key กับ Value น่าจะเป็นตัวเลือกที่ดีกว่า

In [15]:
flattened_dict = {}
for key, values in enum_dict_2.items():
    for sub_key, sub_value in values['sub_keys'].items():
        flattened_key = f"{key}_{sub_key}"
        flattened_dict[flattened_key] = sub_value

print(flattened_dict)
{'MSSubClass_20': '1-STORY 1946 & NEWER ALL STYLES', 'MSSubClass_30': '1-STORY 1945 & OLDER', 'MSSubClass_40': '1-STORY W/FINISHED ATTIC ALL AGES', 'MSSubClass_45': '1-1/2 STORY - UNFINISHED ALL AGES', 'MSSubClass_50': '1-1/2 STORY FINISHED ALL AGES', 'MSSubClass_60': '2-STORY 1946 & NEWER', 'MSSubClass_70': '2-STORY 1945 & OLDER', 'MSSubClass_75': '2-1/2 STORY ALL AGES', 'MSSubClass_80': 'SPLIT OR MULTI-LEVEL', 'MSSubClass_85': 'SPLIT FOYER', 'MSSubClass_90': 'DUPLEX - ALL STYLES AND AGES', 'MSSubClass_120': '1-STORY PUD (Planned Unit Development) - 1946 & NEWER', 'MSSubClass_150': '1-1/2 STORY PUD - ALL AGES', 'MSSubClass_160': '2-STORY PUD - 1946 & NEWER', 'MSSubClass_180': 'PUD - MULTILEVEL - INCL SPLIT LEV/FOYER', 'MSSubClass_190': '2 FAMILY CONVERSION - ALL STYLES AND AGES', 'MSZoning_FV': 'Floating Village Residential', 'MSZoning_RH': 'Residential High Density', 'MSZoning_RL': 'Residential Low Density', 'MSZoning_RP': 'Residential Low Density Park', 'MSZoning_RM': 'Residential Medium Density', 'Street_Grvl': 'Gravel', 'Street_Pave': 'Paved', 'Alley_Grvl': 'Gravel', 'Alley_Pave': 'Paved', 'Alley_NA': 'No alley access', 'LotShape_Reg': 'Regular', 'LotShape_IR1': 'Slightly irregular', 'LotShape_IR2': 'Moderately Irregular', 'LotShape_IR3': 'Irregular', 'LandContour_Lvl': 'Near Flat/Level', 'LandContour_Bnk': 'Banked - Quick and significant rise from street grade to building', 'LandContour_HLS': 'Hillside - Significant slope from side to side', 'LandContour_Low': 'Depression', 'Utilities_AllPub': 'All public Utilities (E,G,W,& S)', 'Utilities_NoSewr': 'Electricity, Gas, and Water (Septic Tank)', 'Utilities_NoSeWa': 'Electricity and Gas Only', 'Utilities_ELO': 'Electricity only', 'LotConfig_Inside': 'Inside lot', 'LotConfig_Corner': 'Corner lot', 'LotConfig_CulDSac': 'Cul-de-sac', 'LotConfig_FR2': 'Frontage on 2 sides of property', 'LotConfig_FR3': 'Frontage on 3 sides of property', 'LandSlope_Gtl': 'Gentle slope', 'LandSlope_Mod': 'Moderate Slope', 'LandSlope_Sev': 'Severe Slope', 'Neighborhood_Blmngtn': 'Bloomington Heights', 'Neighborhood_Blueste': 'Bluestem', 'Neighborhood_BrDale': 'Briardale', 'Neighborhood_BrkSide': 'Brookside', 'Neighborhood_ClearCr': 'Clear Creek', 'Neighborhood_CollgCr': 'College Creek', 'Neighborhood_Crawfor': 'Crawford', 'Neighborhood_Edwards': 'Edwards', 'Neighborhood_Gilbert': 'Gilbert', 'Neighborhood_IDOTRR': 'Iowa DOT and Rail Road', 'Neighborhood_MeadowV': 'Meadow Village', 'Neighborhood_Mitchel': 'Mitchell', 'Neighborhood_Names': 'North Ames', 'Neighborhood_NoRidge': 'Northridge', 'Neighborhood_NPkVill': 'Northpark Villa', 'Neighborhood_NridgHt': 'Northridge Heights', 'Neighborhood_NWAmes': 'Northwest Ames', 'Neighborhood_OldTown': 'Old Town', 'Neighborhood_SWISU': 'South & West of Iowa State University', 'Neighborhood_Sawyer': 'Sawyer', 'Neighborhood_SawyerW': 'Sawyer West', 'Neighborhood_Somerst': 'Somerset', 'Neighborhood_StoneBr': 'Stone Brook', 'Neighborhood_Timber': 'Timberland', 'Neighborhood_Veenker': 'Veenker', 'Condition1_Artery': 'Adjacent to arterial street', 'Condition1_Feedr': 'Adjacent to feeder street', 'Condition1_Norm': 'Normal', 'Condition1_RRNn': "Within 200' of North-South Railroad", 'Condition1_RRAn': 'Adjacent to North-South Railroad', 'Condition1_PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'Condition1_PosA': 'Adjacent to postive off-site feature', 'Condition1_RRNe': "Within 200' of East-West Railroad", 'Condition1_RRAe': 'Adjacent to East-West Railroad', 'Condition2_Artery': 'Adjacent to arterial street', 'Condition2_Feedr': 'Adjacent to feeder street', 'Condition2_Norm': 'Normal', 'Condition2_RRNn': "Within 200' of North-South Railroad", 'Condition2_RRAn': 'Adjacent to North-South Railroad', 'Condition2_PosN': 'Near positive off-site feature--park, greenbelt, etc.', 'Condition2_PosA': 'Adjacent to postive off-site feature', 'Condition2_RRNe': "Within 200' of East-West Railroad", 'Condition2_RRAe': 'Adjacent to East-West Railroad', 'BldgType_1Fam': 'Single-family Detached', 'BldgType_2FmCon': 'Two-family Conversion; originally built as one-family dwelling', 'BldgType_Duplx': 'Duplex', 'BldgType_TwnhsE': 'Townhouse End Unit', 'BldgType_TwnhsI': 'Townhouse Inside Unit', 'HouseStyle_1Story': 'One story', '1.5Unf\tOne and one-half story_2Story': 'Two story', '2.5Unf\tTwo and one-half story_SFoyer': 'Split Foyer', '2.5Unf\tTwo and one-half story_SLvl': 'Split Level', 'OverallQual_10': 'Very Excellent', 'OverallQual_9': 'Excellent', 'OverallQual_8': 'Very Good', 'OverallQual_7': 'Good', 'OverallQual_6': 'Above Average', 'OverallQual_5': 'Average', 'OverallQual_4': 'Below Average', 'OverallQual_3': 'Fair', 'OverallQual_2': 'Poor', 'OverallQual_1': 'Very Poor', 'OverallCond_10': 'Very Excellent', 'OverallCond_9': 'Excellent', 'OverallCond_8': 'Very Good', 'OverallCond_7': 'Good', 'OverallCond_6': 'Above Average', 'OverallCond_5': 'Average', 'OverallCond_4': 'Below Average', 'OverallCond_3': 'Fair', 'OverallCond_2': 'Poor', 'OverallCond_1': 'Very Poor', 'RoofStyle_Flat': 'Flat', 'RoofStyle_Gable': 'Gable', 'RoofStyle_Gambrel': 'Gabrel (Barn)', 'RoofStyle_Hip': 'Hip', 'RoofStyle_Mansard': 'Mansard', 'RoofStyle_Shed': 'Shed', 'RoofMatl_ClyTile': 'Clay or Tile', 'RoofMatl_CompShg': 'Standard (Composite) Shingle', 'RoofMatl_Membran': 'Membrane', 'RoofMatl_Metal': 'Metal', 'RoofMatl_Roll': 'Roll', 'RoofMatl_Tar&Grv': 'Gravel & Tar', 'RoofMatl_WdShake': 'Wood Shakes', 'RoofMatl_WdShngl': 'Wood Shingles', 'Exterior1st_AsbShng': 'Asbestos Shingles', 'Exterior1st_AsphShn': 'Asphalt Shingles', 'Exterior1st_BrkComm': 'Brick Common', 'Exterior1st_BrkFace': 'Brick Face', 'Exterior1st_CBlock': 'Cinder Block', 'Exterior1st_CemntBd': 'Cement Board', 'Exterior1st_HdBoard': 'Hard Board', 'Exterior1st_ImStucc': 'Imitation Stucco', 'Exterior1st_MetalSd': 'Metal Siding', 'Exterior1st_Other': 'Other', 'Exterior1st_Plywood': 'Plywood', 'Exterior1st_PreCast': 'PreCast', 'Exterior1st_Stone': 'Stone', 'Exterior1st_Stucco': 'Stucco', 'Exterior1st_VinylSd': 'Vinyl Siding', 'Exterior1st_Wd Sdng': 'Wood Siding', 'Exterior1st_WdShing': 'Wood Shingles', 'Exterior2nd_AsbShng': 'Asbestos Shingles', 'Exterior2nd_AsphShn': 'Asphalt Shingles', 'Exterior2nd_BrkComm': 'Brick Common', 'Exterior2nd_BrkFace': 'Brick Face', 'Exterior2nd_CBlock': 'Cinder Block', 'Exterior2nd_CemntBd': 'Cement Board', 'Exterior2nd_HdBoard': 'Hard Board', 'Exterior2nd_ImStucc': 'Imitation Stucco', 'Exterior2nd_MetalSd': 'Metal Siding', 'Exterior2nd_Other': 'Other', 'Exterior2nd_Plywood': 'Plywood', 'Exterior2nd_PreCast': 'PreCast', 'Exterior2nd_Stone': 'Stone', 'Exterior2nd_Stucco': 'Stucco', 'Exterior2nd_VinylSd': 'Vinyl Siding', 'Exterior2nd_Wd Sdng': 'Wood Siding', 'Exterior2nd_WdShing': 'Wood Shingles', 'MasVnrType_BrkCmn': 'Brick Common', 'MasVnrType_BrkFace': 'Brick Face', 'MasVnrType_CBlock': 'Cinder Block', 'MasVnrType_None': 'None', 'MasVnrType_Stone': 'Stone', 'ExterQual_Ex': 'Excellent', 'ExterQual_Gd': 'Good', 'ExterQual_TA': 'Average/Typical', 'ExterQual_Fa': 'Fair', 'ExterQual_Po': 'Poor', 'ExterCond_Ex': 'Excellent', 'ExterCond_Gd': 'Good', 'ExterCond_TA': 'Average/Typical', 'ExterCond_Fa': 'Fair', 'ExterCond_Po': 'Poor', 'Foundation_BrkTil': 'Brick & Tile', 'Foundation_CBlock': 'Cinder Block', 'Foundation_PConc': 'Poured Contrete', 'Foundation_Slab': 'Slab', 'Foundation_Stone': 'Stone', 'Foundation_Wood': 'Wood', 'BsmtQual_Ex': 'Excellent (100+ inches)', 'BsmtQual_Gd': 'Good (90-99 inches)', 'BsmtQual_TA': 'Typical (80-89 inches)', 'BsmtQual_Fa': 'Fair (70-79 inches)', 'BsmtQual_Po': 'Poor (<70 inches', 'BsmtQual_NA': 'No Basement', 'BsmtCond_Ex': 'Excellent', 'BsmtCond_Gd': 'Good', 'BsmtCond_TA': 'Typical - slight dampness allowed', 'BsmtCond_Fa': 'Fair - dampness or some cracking or settling', 'BsmtCond_Po': 'Poor - Severe cracking, settling, or wetness', 'BsmtCond_NA': 'No Basement', 'BsmtExposure_Gd': 'Good Exposure', 'BsmtExposure_Av': 'Average Exposure (split levels or foyers typically score average or above)', 'BsmtExposure_Mn': 'Mimimum Exposure', 'BsmtExposure_No': 'No Exposure', 'BsmtExposure_NA': 'No Basement', 'BsmtFinType1_GLQ': 'Good Living Quarters', 'BsmtFinType1_ALQ': 'Average Living Quarters', 'BsmtFinType1_BLQ': 'Below Average Living Quarters', 'BsmtFinType1_Rec': 'Average Rec Room', 'BsmtFinType1_LwQ': 'Low Quality', 'BsmtFinType1_Unf': 'Unfinshed', 'BsmtFinType1_NA': 'No Basement', 'BsmtFinType2_GLQ': 'Good Living Quarters', 'BsmtFinType2_ALQ': 'Average Living Quarters', 'BsmtFinType2_BLQ': 'Below Average Living Quarters', 'BsmtFinType2_Rec': 'Average Rec Room', 'BsmtFinType2_LwQ': 'Low Quality', 'BsmtFinType2_Unf': 'Unfinshed', 'BsmtFinType2_NA': 'No Basement', 'Heating_Floor': 'Floor Furnace', 'Heating_GasA': 'Gas forced warm air furnace', 'Heating_GasW': 'Gas hot water or steam heat', 'Heating_Grav': 'Gravity furnace', 'Heating_OthW': 'Hot water or steam heat other than gas', 'Heating_Wall': 'Wall furnace', 'HeatingQC_Ex': 'Excellent', 'HeatingQC_Gd': 'Good', 'HeatingQC_TA': 'Average/Typical', 'HeatingQC_Fa': 'Fair', 'HeatingQC_Po': 'Poor', 'CentralAir_N': 'No', 'CentralAir_Y': 'Yes', 'Electrical_SBrkr': 'Standard Circuit Breakers & Romex', 'Electrical_FuseA': 'Fuse Box over 60 AMP and all Romex wiring (Average)', 'Electrical_FuseF': '60 AMP Fuse Box and mostly Romex wiring (Fair)', 'Electrical_FuseP': '60 AMP Fuse Box and mostly knob & tube wiring (poor)', 'Electrical_Mix': 'Mixed', 'KitchenQual_Ex': 'Excellent', 'KitchenQual_Gd': 'Good', 'KitchenQual_TA': 'Typical/Average', 'KitchenQual_Fa': 'Fair', 'KitchenQual_Po': 'Poor', 'Functional_Typ': 'Typical Functionality', 'Functional_Min1': 'Minor Deductions 1', 'Functional_Min2': 'Minor Deductions 2', 'Functional_Mod': 'Moderate Deductions', 'Functional_Maj1': 'Major Deductions 1', 'Functional_Maj2': 'Major Deductions 2', 'Functional_Sev': 'Severely Damaged', 'Functional_Sal': 'Salvage only', 'FireplaceQu_Ex': 'Excellent - Exceptional Masonry Fireplace', 'FireplaceQu_Gd': 'Good - Masonry Fireplace in main level', 'FireplaceQu_TA': 'Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement', 'FireplaceQu_Fa': 'Fair - Prefabricated Fireplace in basement', 'FireplaceQu_Po': 'Poor - Ben Franklin Stove', 'FireplaceQu_NA': 'No Fireplace', 'GarageType_2Types': 'More than one type of garage', 'GarageType_Attchd': 'Attached to home', 'GarageType_Basment': 'Basement Garage', 'GarageType_BuiltIn': 'Built-In (Garage part of house - typically has room above garage)', 'GarageType_CarPort': 'Car Port', 'GarageType_Detchd': 'Detached from home', 'GarageType_NA': 'No Garage', 'GarageFinish_Fin': 'Finished', 'GarageFinish_RFn': 'Rough Finished', 'GarageFinish_Unf': 'Unfinished', 'GarageFinish_NA': 'No Garage', 'GarageQual_Ex': 'Excellent', 'GarageQual_Gd': 'Good', 'GarageQual_TA': 'Typical/Average', 'GarageQual_Fa': 'Fair', 'GarageQual_Po': 'Poor', 'GarageQual_NA': 'No Garage', 'GarageCond_Ex': 'Excellent', 'GarageCond_Gd': 'Good', 'GarageCond_TA': 'Typical/Average', 'GarageCond_Fa': 'Fair', 'GarageCond_Po': 'Poor', 'GarageCond_NA': 'No Garage', 'PavedDrive_Y': 'Paved', 'PavedDrive_P': 'Partial Pavement', 'PavedDrive_N': 'Dirt/Gravel', 'PoolQC_Ex': 'Excellent', 'PoolQC_Gd': 'Good', 'PoolQC_TA': 'Average/Typical', 'PoolQC_Fa': 'Fair', 'PoolQC_NA': 'No Pool', 'Fence_GdPrv': 'Good Privacy', 'Fence_MnPrv': 'Minimum Privacy', 'Fence_GdWo': 'Good Wood', 'Fence_MnWw': 'Minimum Wood/Wire', 'Fence_NA': 'No Fence', 'MiscFeature_Elev': 'Elevator', 'MiscFeature_Gar2': '2nd Garage (if not described in garage section)', 'MiscFeature_Othr': 'Other', 'MiscFeature_Shed': 'Shed (over 100 SF)', 'MiscFeature_TenC': 'Tennis Court', 'MiscFeature_NA': 'None', 'SaleType_WD': 'Warranty Deed - Conventional', 'SaleType_CWD': 'Warranty Deed - Cash', 'SaleType_VWD': 'Warranty Deed - VA Loan', 'SaleType_New': 'Home just constructed and sold', 'SaleType_COD': 'Court Officer Deed/Estate', 'SaleType_Con': 'Contract 15% Down payment regular terms', 'SaleType_ConLw': 'Contract Low Down payment and low interest', 'SaleType_ConLI': 'Contract Low Interest', 'SaleType_ConLD': 'Contract Low Down', 'SaleType_Oth': 'Other', 'SaleCondition_Normal': 'Normal Sale', 'SaleCondition_Abnorml': 'Abnormal Sale -  trade, foreclosure, short sale', 'SaleCondition_AdjLand': 'Adjoining Land Purchase', 'SaleCondition_Alloca': 'Allocation - two linked properties with separate deeds, typically condo with a garage unit', 'SaleCondition_Family': 'Sale between family members', 'SaleCondition_Partial': 'Home was not completed when last assessed (associated with New Homes)'}

มาท่อนนี้เราจะทำการตัด Dictionary ในตัว Main_key ด้วยการ Split โดยตัวด้านหน้าหมายถึงชื่อ Columns จากนั้นค่อยเปลี่ยนตัวแปรด้วย การใช้ df[col].map แต่ทั้งนี้ใน map เราต้องเขียน lambda function เพื่อเอามาแค่ตัวที่เราจะใช้เท่านั้น

โดย Pattern คือ เราจะมองหาว่า main_key ที่เราตัดมา และตัว x ใน lambda function พอใส่เข้าไปแล้ว มันไปตรงกับ key ใน flattened_dict หรือไม่ ถ้าใช่ก็จะทำการ Map key ให้ตรงต่อไป แต่ถ้าไม่ใช่ก็จะ Return ค่าเดิมออกมา

หมายเหตุ : การใช้ map ใน Pandas จะทำให้ ตัวแปร main_key ถูกส่งผ่านไปยัง lambda function และ main_key จะกลายเป็นตัวแปร x ไปในทันที

In [16]:
for key in flattened_dict.keys():
    main_key = key.split('_')[0]  # แกะ Main Key ออกมา เช่นเอา "MSSubClass" ออกมาจาก "MSSubClass_20"
    if main_key in df.columns:  # เช็คว่า Main Key ตรงนี้เป็น Column ของ Dataframe หรือไม่ ถ้าใช่เราจะเปลี่ยนตัวแปรด้วยการทำ Map
        df[main_key] = df[main_key].map(lambda x: flattened_dict.get(f"{main_key}_{x}", x))


df.head()
Out[16]:
Id MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape LandContour Utilities ... PoolArea PoolQC Fence MiscFeature MiscVal MoSold YrSold SaleType SaleCondition SalePrice
0 1 2-STORY 1946 & NEWER Residential Low Density 65.0 8450 Paved No alley access Regular Near Flat/Level All public Utilities (E,G,W,& S) ... 0 No Pool No Fence None 0 2 2008 Warranty Deed - Conventional Normal Sale 208500
1 2 1-STORY 1946 & NEWER ALL STYLES Residential Low Density 80.0 9600 Paved No alley access Regular Near Flat/Level All public Utilities (E,G,W,& S) ... 0 No Pool No Fence None 0 5 2007 Warranty Deed - Conventional Normal Sale 181500
2 3 2-STORY 1946 & NEWER Residential Low Density 68.0 11250 Paved No alley access Slightly irregular Near Flat/Level All public Utilities (E,G,W,& S) ... 0 No Pool No Fence None 0 9 2008 Warranty Deed - Conventional Normal Sale 223500
3 4 2-STORY 1945 & OLDER Residential Low Density 60.0 9550 Paved No alley access Slightly irregular Near Flat/Level All public Utilities (E,G,W,& S) ... 0 No Pool No Fence None 0 2 2006 Warranty Deed - Conventional Abnormal Sale - trade, foreclosure, short sale 140000
4 5 2-STORY 1946 & NEWER Residential Low Density 84.0 14260 Paved No alley access Slightly irregular Near Flat/Level All public Utilities (E,G,W,& S) ... 0 No Pool No Fence None 0 12 2008 Warranty Deed - Conventional Normal Sale 250000

5 rows × 81 columns

โอเค ! ดูเหมือนผลลัพธ์ใช้ได้ ทีนี้ลองมา print สแกนๆ ดูดีกว่าว่าเป็นอย่างไร

In [17]:
df_object_cols = df.select_dtypes(include=['object']).columns

for col in df_object_cols:
    print(f"Column '{col}' unique values: ", df[col].unique())
Column 'MSSubClass' unique values:  ['2-STORY 1946 & NEWER' '1-STORY 1946 & NEWER ALL STYLES'
 '2-STORY 1945 & OLDER' '1-1/2 STORY FINISHED ALL AGES'
 '2 FAMILY CONVERSION - ALL STYLES AND AGES'
 '1-1/2 STORY - UNFINISHED ALL AGES' 'DUPLEX - ALL STYLES AND AGES'
 '1-STORY PUD (Planned Unit Development) - 1946 & NEWER'
 '1-STORY 1945 & OLDER' 'SPLIT FOYER' 'SPLIT OR MULTI-LEVEL'
 '2-STORY PUD - 1946 & NEWER' '2-1/2 STORY ALL AGES'
 'PUD - MULTILEVEL - INCL SPLIT LEV/FOYER'
 '1-STORY W/FINISHED ATTIC ALL AGES']
Column 'MSZoning' unique values:  ['Residential Low Density' 'Residential Medium Density' 'C (all)'
 'Floating Village Residential' 'Residential High Density']
Column 'Street' unique values:  ['Paved' 'Gravel']
Column 'Alley' unique values:  ['No alley access' 'Gravel' 'Paved']
Column 'LotShape' unique values:  ['Regular' 'Slightly irregular' 'Moderately Irregular' 'Irregular']
Column 'LandContour' unique values:  ['Near Flat/Level'
 'Banked - Quick and significant rise from street grade to building'
 'Depression' 'Hillside - Significant slope from side to side']
Column 'Utilities' unique values:  ['All public Utilities (E,G,W,& S)' 'Electricity and Gas Only']
Column 'LotConfig' unique values:  ['Inside lot' 'Frontage on 2 sides of property' 'Corner lot' 'Cul-de-sac'
 'Frontage on 3 sides of property']
Column 'LandSlope' unique values:  ['Gentle slope' 'Moderate Slope' 'Severe Slope']
Column 'Neighborhood' unique values:  ['College Creek' 'Veenker' 'Crawford' 'Northridge' 'Mitchell' 'Somerset'
 'Northwest Ames' 'Old Town' 'Brookside' 'Sawyer' 'Northridge Heights'
 'NAmes' 'Sawyer West' 'Iowa DOT and Rail Road' 'Meadow Village' 'Edwards'
 'Timberland' 'Gilbert' 'Stone Brook' 'Clear Creek' 'Northpark Villa'
 'Bloomington Heights' 'Briardale' 'South & West of Iowa State University'
 'Bluestem']
Column 'Condition1' unique values:  ['Normal' 'Adjacent to feeder street'
 'Near positive off-site feature--park, greenbelt, etc.'
 'Adjacent to arterial street' 'Adjacent to East-West Railroad'
 "Within 200' of North-South Railroad" 'Adjacent to North-South Railroad'
 'Adjacent to postive off-site feature'
 "Within 200' of East-West Railroad"]
Column 'Condition2' unique values:  ['Normal' 'Adjacent to arterial street'
 "Within 200' of North-South Railroad" 'Adjacent to feeder street'
 'Near positive off-site feature--park, greenbelt, etc.'
 'Adjacent to postive off-site feature' 'Adjacent to North-South Railroad'
 'Adjacent to East-West Railroad']
Column 'BldgType' unique values:  ['Single-family Detached' '2fmCon' 'Duplex' 'Townhouse End Unit' 'Twnhs']
Column 'HouseStyle' unique values:  ['2Story' 'One story' '1.5Fin' '1.5Unf' 'SFoyer' 'SLvl' '2.5Unf' '2.5Fin']
Column 'OverallQual' unique values:  ['Good' 'Above Average' 'Very Good' 'Average' 'Excellent' 'Below Average'
 'Very Excellent' 'Fair' 'Very Poor' 'Poor']
Column 'OverallCond' unique values:  ['Average' 'Very Good' 'Above Average' 'Good' 'Below Average' 'Poor'
 'Fair' 'Excellent' 'Very Poor']
Column 'RoofStyle' unique values:  ['Gable' 'Hip' 'Gabrel (Barn)' 'Mansard' 'Flat' 'Shed']
Column 'RoofMatl' unique values:  ['Standard (Composite) Shingle' 'Wood Shingles' 'Metal' 'Wood Shakes'
 'Membrane' 'Gravel & Tar' 'Roll' 'Clay or Tile']
Column 'Exterior1st' unique values:  ['Vinyl Siding' 'Metal Siding' 'Wood Siding' 'Hard Board' 'Brick Face'
 'Wood Shingles' 'Cement Board' 'Plywood' 'Asbestos Shingles' 'Stucco'
 'Brick Common' 'Asphalt Shingles' 'Stone' 'Imitation Stucco'
 'Cinder Block']
Column 'Exterior2nd' unique values:  ['Vinyl Siding' 'Metal Siding' 'Wd Shng' 'Hard Board' 'Plywood'
 'Wood Siding' 'CmentBd' 'Brick Face' 'Stucco' 'Asbestos Shingles'
 'Brk Cmn' 'Imitation Stucco' 'Asphalt Shingles' 'Stone' 'Other'
 'Cinder Block']
Column 'MasVnrType' unique values:  ['Brick Face' 'None' 'Stone' 'Brick Common']
Column 'ExterQual' unique values:  ['Good' 'Average/Typical' 'Excellent' 'Fair']
Column 'ExterCond' unique values:  ['Average/Typical' 'Good' 'Fair' 'Poor' 'Excellent']
Column 'Foundation' unique values:  ['Poured Contrete' 'Cinder Block' 'Brick & Tile' 'Wood' 'Slab' 'Stone']
Column 'BsmtQual' unique values:  ['Good (90-99 inches)' 'Typical (80-89 inches)' 'Excellent (100+ inches)'
 'No Basement' 'Fair (70-79 inches)']
Column 'BsmtCond' unique values:  ['Typical - slight dampness allowed' 'Good' 'No Basement'
 'Fair - dampness or some cracking or settling'
 'Poor - Severe cracking, settling, or wetness']
Column 'BsmtExposure' unique values:  ['No Exposure' 'Good Exposure' 'Mimimum Exposure'
 'Average Exposure (split levels or foyers typically score average or above)'
 'No Basement']
Column 'BsmtFinType1' unique values:  ['Good Living Quarters' 'Average Living Quarters' 'Unfinshed'
 'Average Rec Room' 'Below Average Living Quarters' 'No Basement'
 'Low Quality']
Column 'BsmtFinType2' unique values:  ['Unfinshed' 'Below Average Living Quarters' 'No Basement'
 'Average Living Quarters' 'Average Rec Room' 'Low Quality'
 'Good Living Quarters']
Column 'Heating' unique values:  ['Gas forced warm air furnace' 'Gas hot water or steam heat'
 'Gravity furnace' 'Wall furnace' 'Hot water or steam heat other than gas'
 'Floor Furnace']
Column 'HeatingQC' unique values:  ['Excellent' 'Good' 'Average/Typical' 'Fair' 'Poor']
Column 'CentralAir' unique values:  ['Yes' 'No']
Column 'Electrical' unique values:  ['Standard Circuit Breakers & Romex'
 '60 AMP Fuse Box and mostly Romex wiring (Fair)'
 'Fuse Box over 60 AMP and all Romex wiring (Average)'
 '60 AMP Fuse Box and mostly knob & tube wiring (poor)' 'Mixed' 'NA']
Column 'KitchenQual' unique values:  ['Good' 'Typical/Average' 'Excellent' 'Fair']
Column 'Functional' unique values:  ['Typical Functionality' 'Minor Deductions 1' 'Major Deductions 1'
 'Minor Deductions 2' 'Moderate Deductions' 'Major Deductions 2'
 'Severely Damaged']
Column 'FireplaceQu' unique values:  ['No Fireplace'
 'Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement'
 'Good - Masonry Fireplace in main level'
 'Fair - Prefabricated Fireplace in basement'
 'Excellent - Exceptional Masonry Fireplace' 'Poor - Ben Franklin Stove']
Column 'GarageType' unique values:  ['Attached to home' 'Detached from home'
 'Built-In (Garage part of house - typically has room above garage)'
 'Car Port' 'No Garage' 'Basement Garage' 'More than one type of garage']
Column 'GarageFinish' unique values:  ['Rough Finished' 'Unfinished' 'Finished' 'No Garage']
Column 'GarageQual' unique values:  ['Typical/Average' 'Fair' 'Good' 'No Garage' 'Excellent' 'Poor']
Column 'GarageCond' unique values:  ['Typical/Average' 'Fair' 'No Garage' 'Good' 'Poor' 'Excellent']
Column 'PavedDrive' unique values:  ['Paved' 'Dirt/Gravel' 'Partial Pavement']
Column 'PoolQC' unique values:  ['No Pool' 'Excellent' 'Fair' 'Good']
Column 'Fence' unique values:  ['No Fence' 'Minimum Privacy' 'Good Wood' 'Good Privacy'
 'Minimum Wood/Wire']
Column 'MiscFeature' unique values:  ['None' 'Shed (over 100 SF)'
 '2nd Garage (if not described in garage section)' 'Other' 'Tennis Court']
Column 'SaleType' unique values:  ['Warranty Deed - Conventional' 'Home just constructed and sold'
 'Court Officer Deed/Estate' 'Contract Low Down' 'Contract Low Interest'
 'Warranty Deed - Cash' 'Contract Low Down payment and low interest'
 'Contract 15% Down payment regular terms' 'Other']
Column 'SaleCondition' unique values:  ['Normal Sale' 'Abnormal Sale -  trade, foreclosure, short sale'
 'Home was not completed when last assessed (associated with New Homes)'
 'Adjoining Land Purchase'
 'Allocation - two linked properties with separate deeds, typically condo with a garage unit'
 'Sale between family members']

ดูจาก Output แล้วไม่น่ามีปัญหาอะไร เข้าสู่ Part ถัดไปกันดีกว่า

Part 2 : หาคำตอบผ่านการ Visualization¶

ใน Part นี้เราจะพูดถึงการทำความเข้าใจข้อมูลในด้านต่างๆ โดยหลักๆ จะเน้นไปที่

  • Descriptive Analytics : เน้นไปที่การดูข้อมูลต่างๆ และมองหา Insight โดยไม่เข้าใจว่าทำไม
  • Diagnostics Analytics : วิเคราะห์สาเหตุต่อจาก Descriptive อีกทีว่าทำไมถึงเป็นอย่างนั้น โดยส่วนตัวคิดว่าข้อมูลที่ให้มาไม่เพียงพอที่จะตอบคำถามได้อย่างชัดเจน แต่จะพยายามเดาจาก Context ที่มีอยู่

ส่วนใน Part ของฝั่ง

  • Predictive Analytics : ทำนายอนาคตด้วย Machine Learning Model
  • Prescriptive Analytics : เอาคำตอบจาก Model ต่างๆมาทำเป็น Solution รูปแบบต่างๆ

ไว้มีโอกาสจะมาทำให้นะครับ 😆

กลับเข้าเรื่องของการ Visualized สิ่งแรกที่ต้องทำคงจะหนีไม่พ้นการ Import Library ที่ทำงานด้านนี้ได้อย่างดีเยี่ยมทั้งสองตัวอย่าง Seaborn และ Matplotlib

In [18]:
import seaborn as sns
import matplotlib.pyplot as plt
In [19]:
#import ไปก่อนเพื่อได้ใช้
import warnings
warnings.filterwarnings("ignore")

ลองทดสอบหา Correlations ดู¶

In [20]:
plt.figure(figsize=(12,8))
sns.heatmap(df.corr("pearson"),cmap="RdBu")
plt.title("Correlations Between Variables", size=15)
plt.show()

ลองตรวจสอบการกระจายตัวของราคาบ้านว่ามีการกระจายตัวแบบใด ?¶

เราลอง Plot SalePrice ใน 3 รูปแบบดังนี้

  • Johnson SU
  • Normal
  • Log Normal

แล้วดูว่าการกระจายตัวของข้อมูลของเรามันทับกับแบบไหน

Note : ถึงแม้คราวนี้จะไม่ได้ทำเกี่ยวกับ Machine Learning แต่โดยปกติแล้ว ในการทำ Machine Learning Model ส่วนใหญ่มักจะชอบข้อมูลที่มีการกระจายตัวแบบ Normal Distribution ซึ่งถ้าข้อมูลดิบที่ให้มามันไม่ได้กระจายตัวแบบนั้น ก็ต้องแปลงร่างมันอีกที

In [21]:
import scipy.stats as st
y = df['SalePrice']
plt.figure(1); plt.title('Johnson SU')
sns.distplot(y, kde=True, fit=st.johnsonsu)
plt.figure(2); plt.title('Normal')
sns.distplot(y, kde=True, fit=st.norm)
plt.figure(3); plt.title('Log Normal')
sns.distplot(y, kde=True, fit=st.lognorm)
Out[21]:
<Axes: title={'center': 'Log Normal'}, xlabel='SalePrice', ylabel='Density'>

จะเห็นว่าการกระจายของเราเป็น Log Normal ไม่ก็ Johnson SU

การทำ ANOVA(Analysis of Variance)¶

สำหรับการตรวจสอบ Sensitivity ปัญหาของข้อมูลประเภท Categorial คือมันไม่สามารถจัดลำดับมันได้โดยตรง

ทำให้เราต้องอาศัยเทคนิคทางสถิติที่เรียกว่า ANOVA ที่มีหลักการคือการหาค่าเฉลี่ยของแต่ละกลุ่ม และค่าเฉลี่ยของทุกคน แล้วมาเทียบกันว่า พอตัวแปรเปลี่ยนกลุ่มแล้ว ค่าที่ได้มันเบี่ยงเบนออกจากค่าเฉลี่ยเดิมมากน้อยขนาดไหน ถ้าเบี่ยงมากแปลว่าตัวแปรนี้มีผลต่อค่าเฉลี่ยสูง เป็นต้น

โดยเราต้องใช้ Library อย่าง numpy และ scipy เข้ามาช่วย (ไม่รู้ Import ด้านบนไปหรือยัง)

In [22]:
import scipy.stats as stats
import numpy as np

categorical_columns = df.select_dtypes(include=['object']).columns

anova_results = []

for column in categorical_columns:
    groups = [df['SalePrice'][df[column] == category] for category in df[column].unique()]
    f_val, p_val = stats.f_oneway(*groups)

    anova_results.append({'Variable': column, 'F-value': f_val, 'p-value': p_val})

anova_results_df = pd.DataFrame(anova_results)


plt.figure(figsize=(10, 6))


sns.barplot(data=anova_results_df, y='Variable', x=-np.log10(anova_results_df['p-value']))

plt.xlabel('-Log10(p-value)')
plt.title('ANOVA results')
plt.show()

เรียงลำดับความแรง โดยใช้ทั้ง f-Value และ p-value¶

ปกติแล้วการทำ ANOVA ทั้ง f-value และ p-value ต่างเอามาใช้บอกความแรง(Sensitivity) ได้ทั้งคู่ ดังนั้นเพื่อคมุมมองที่มากกว่า เราจะใช้ทั้งคู่ไปเลย

โดยที่

  • p-value น้อยๆ ยิ่งแรงเพราะแปลว่าเราปฏิเสธ Null Hypothesis ของเราได้มาก (Null ของเราในทีนี้คือ Mean ไม่เปลี่ยนเมื่อเปลี่ยนกลุ่ม)
  • f-value ยิ่งมากยิ่งแรง เพราะ f-value หมายถึงอัตราส่วนความแปรปรวนระหว่างกลุ่มเทียบกับอัตราส่วนความแปรปรวนภายในกลุ่ม

ทั้งนี้สำหรับการเรียงลำดับ เราจึงนำเอา p-value มาปรับเป็น -log scale อีกทีเพื่อให้ค่า p-value ที่น้อยที่สุดแสดงผลได้มากที่สุด

In [23]:
anova_results_df = anova_results_df.sort_values(by='p-value', ascending=True)

plt.figure(figsize=(10,6))


sns.barplot(data=anova_results_df, x='Variable', y=-np.log10(anova_results_df['p-value']))

plt.xlabel('-Log10(p-value)')
plt.title('ANOVA p-values for Categorical Variables')
x=plt.xticks(rotation=90)
plt.show()
In [24]:
anova_results_df = anova_results_df.sort_values(by='F-value', ascending=False)

plt.figure(figsize=(10,6))


sns.barplot(data=anova_results_df, x='Variable', y='F-value')

plt.xlabel('F-value')
plt.title('ANOVA F-values for Categorical Variables')
x=plt.xticks(rotation=90)
plt.show()

สรุปผลการทำ ANOVA¶

หลังจากทำ Anova จะเห็นว่าตัวแปร ExterQual,OverallQual,BsmentQual,KitchenQual,Neighberhood

ดูจะเป็นตัวแปรที่ส่งผลกระทบกับ SalePrice อย่างมีนัยยะสำคัญ ในขณะที่ตัวแปรอย่างเช่นสาธารณูปโภค การติด Heating หรือ Aircondition ดูไม่ค่อยสำคัญเท่าไร

ทั้งนี้ถ้าให้เดาคิดว่าตัวแปรที่ไม่ค่อยสำคัญ น่าจะเป็นอะไรที่มัน Common สำหรับทุกบ้าน หรืออาจจะไม่ต่างกันมากในแต่ละกลุ่ม

ตัวแปรไหนส่งผลต่อ SalePrice มากที่สุด เมื่อใช้ Spearman Correlation?¶

อีกวิธีที่ดูความสัมพันธ์ระหว่างตัวแปรคือการหา Correlation ทั้งนี้เราเลือกใช้ Spearman Correlation ในการตรวจสอบว่าตัวแปรไหนส่งผลต่อ SalePrice มากที่สุด เนื่องจาก Spearman Correlation เหมาะสำหรับตรวจสอบตัวแปรสองตัวที่มีแนวโน้มเคลื่อนที่ตามกัน และจุดที่สำคัญคือตัวแปรสองตัวไม่จำเป็นต้องมีความสัมพันธ์แบบเส้นตรงแบบ Pearson Correlation

แต่อย่าลืมว่าตัวแปรบางอย่างไม่ได้เป็นตัวแปรที่เป็นตัวเลข ดังนั้นวิธีการที่เราต้องใช้จัดการกับข้อมูลเชิงคุณภาพคือ การหาค่าเฉลี่ยของราคาแต่ละกลุ่ม จากนั้นเอาค่าเฉลี่ยของราคาบ้านในแต่ละกลุ่มตัวแปรมาทำ Correlation เพื่อหาความสัมพันธ์อีกที

In [25]:
df_copy = df.copy()


categorical_columns = df_copy.select_dtypes(include=['object']).columns

# เปลี่ยน Categorial Data แต่ละตัวด้วยค่าเฉลี่ยแทน
for column in categorical_columns:
    mean_saleprice = df_copy.groupby(column)['SalePrice'].mean()
    df_copy[column] = df_copy[column].map(mean_saleprice)

# แปลงข้อมูลให้เป็น Numeric แล้วทำ Spearman
spearman_corr_df = df_copy.corr(method='spearman')['SalePrice'].sort_values(ascending=False).reset_index()
spearman_corr_df.columns = ['Feature', 'Spearman_Correlation']


spearman_corr_df = spearman_corr_df[spearman_corr_df['Feature'] != 'SalePrice']


plt.figure(figsize=(10, len(spearman_corr_df) / 5))
sns.barplot(data=spearman_corr_df, y='Feature', x='Spearman_Correlation', orient='h')
plt.title('Spearman Correlation of Features with SalePrice')
plt.show()

และด้วยความที่ตัวแปรมันเยอะมาก และค่า Correlation มีทั้งบวกและลบ ผมเลยตัดมาแค่ตัวแปรที่สำคัญที่สุด 15 อันดับแรกที่มีค่า Correlation สูงสุด เพื่อมาหาว่าตัวแปรไหนส่งผลต่อราคาขายบ้านมากที่สุด

In [26]:
spearman_corr_df = spearman_corr_df.head(15)


plt.figure(figsize=(10, len(spearman_corr_df) / 2))
sns.barplot(data=spearman_corr_df, y='Feature', x='Spearman_Correlation', orient='h')
plt.title('Top 15 Features by Spearman Correlation with SalePrice')
plt.show()

จะเห็นว่าตัวแปรในกลุ่มที่เป็นคุณภาพ ก็ได้ผลลัพธ์ค่าเดิมๆ คล้ายๆกับที่ทำ ANOVA ในขณะที่ตัวแปรที่เป็นตัวเลขก็จะเกี่ยวข้องกับพื้นที่ของตัวบ้าน เช่น

  • GrLivArea = Above grade (ground) living area square feet พื้นที่ใช้สอยบนดิน
  • GarageCars = Size of garage in car capacity จำนวนรถที่จอดได้

อีกตัวแปรที่น่าสนใจไม่แพ้กันคือปีที้สร้างหรือ YearBuilt

คำถามที่จะถาม Data ต่อ¶

ผมสมมุติว่าได้ข้อมูลมาก้อนหนึ่ง โดยที่ไม่ได้รู้ Context อะไรเลยเกี่ยวกับข้อมูล

หลังจากที่ทำ ANOVA แล้ว ยังมีคำถามที่ผมสงสัย(อันนี้สงสัยเอง) เลยจะลองไล่หาคำตอบดูครับ

คำถามที่สงสัยได้แก่

  • Trend ที่อยู่อาศัยในแต่ละยุคเป็นอย่างไร ที่ไหนฮิต
  • ย่านไหน ใครอยู่ ?
  • มีบ้านที่มีสระว่ายน้ำกี่%
  • ยิ่งบ้านยิ่งใหม่ ยิ่งต้องมีโรงจอดรถจริงไหม
  • ปกติบ้าน Renovate กันเมื่อไร
  • Trend บ้านในแต่ละยุคเป็นอย่างไร

บรรทัดนี้พอดีจะ Map Dictionary ใหม่ เพราะพึ่งเห็นว่า OverallQual มันเป็นคะแนน ควรจะเรียงถึง 1-10

In [27]:
nested_dict = enum_dict_2['OverallQual']
OverallQualMapping = nested_dict['sub_keys']


inverted_mapping = {v: k for k, v in OverallQualMapping.items()}


df['OverallQualScore'] = df['OverallQual'].map(inverted_mapping).astype(int)

ลอง Plot ระหว่าง SalePrice vs OverallQual¶

In [28]:
import seaborn as sns

sorted_labels = sorted(inverted_mapping, key=lambda k: int(inverted_mapping[k]))



plt.figure(figsize=(12, 6))
sns.boxplot(x='OverallQual', y='SalePrice', data=df,order=sorted_labels)

plt.title('Sale Price vs Overall Quality')
plt.xlabel('Overall Quality')
plt.ylabel('Sale Price')

plt.show()
In [29]:
quality_columns = []
non_quality_columns = []


for column in categorical_columns:
    if column.endswith('Qual') or column.endswith('QC') or column.endswith('Qu'):
        quality_columns.append(column)
    else:
        non_quality_columns.append(column)

ลองทำ BoxPlot ของหลายๆ ข้อมูลเพื่อสแกนดูคร่าวๆ¶

ปกติแล้วสำหรับ Categorial Data ผมจะชอบ Plot ข้อมูลออกมาด้วย Box Plot แล้วลองดูว่ามีข้อมูลชุดไหนบ้างที่น่าสนใจค้นคว้าต่อ

Note : ลอง Plot แบบปกติแล้วพบว่าดูไม่รู้เรื่องเลย เลยต้องเปลี่ยนเป็นการทำ BoxPlot ในแกน Y แทน

In [30]:
def draw_horizontal_boxplots(data, y_col):
    plt.figure(figsize=(10, len(data.columns) * 4))

    for i, col in enumerate(data.columns):
        if col != y_col:
            plt.subplot(len(data.columns)-1, 1, i+1)


            ax = sns.boxplot(x=y_col, y=col, data=data, orient='h')


            ax.set_ylabel(col, rotation=0, va='bottom', ha='right')

            plt.tight_layout()

feature_only = df[non_quality_columns]
feature_only['SalePrice'] = df['SalePrice']

draw_horizontal_boxplots(feature_only, 'SalePrice')
In [31]:
qual_only = df[quality_columns]
qual_only['SalePrice']=df['SalePrice']
draw_horizontal_boxplots(qual_only, 'SalePrice')

บ้านส่วนใหญ่ที่ Good Quality พื้นที่จะอยู่ในช่วง 1000-2000 sqr-feet¶

ลองทำ Scatter Plot จะเห็นว่าบ้านส่วนใหญ่คุณภาพดี ราคาอยู่ในโซน 2-3 แสนเหรียญ และพื้นที่ก็อยู่ในช่วง 1000-2000 sqr-feet

Note : 1000 sqr-feet = 92.9 ตารางเมตร

In [32]:
plt.figure(figsize=(12, 8))
sns.scatterplot(data=df, x='GrLivArea', y='SalePrice', hue='OverallQual', palette='viridis', sizes=(20, 200), alpha=0.7)
plt.title('Scatterplot of SalePrice vs GrLivArea colored by OverallQual')
plt.show()

คนให้ความสำคัญกับครัวมากขึ้นเรื่อยๆ¶

อีกตัวแปรที่ Sensitivity ไวพอๆ กับ GrLiving คือคุณภาพครัว ผมคิดว่าคนสมัยใหม่ชอบทำกับข้าว ดังนั้นปัจจัยนี้น่าจะเป็นตัวเลือกที่สำคัญไม่แพ้กัน

ด้วยความอยากรู้ผมเลย Plot Percentage ว่าสัดส่วนบ้านที่สร้างในแต่ละยุค คุณภาพของครัวเป็นอย่างไร จะเห็นว่าในช่วงยุค 1970s เราจะเริ่มเห็นครัวแบบ Good และพอมายุค 2000s เราเริ่มเห็นครัวแบบ Excellent เป็นสัดส่วนที่มากขึ้น

In [33]:
df['DecadeBuilt'] = (10 * (df['YearBuilt'] // 10)).astype(str) + 's'

DecadeBuilt_KitchenQual_grouped = df.groupby(['DecadeBuilt', 'KitchenQual']).size().reset_index(name='Count')


pivot_data = DecadeBuilt_KitchenQual_grouped.pivot(index='DecadeBuilt', columns='KitchenQual', values='Count').fillna(0)


pivot_percentage = pivot_data.divide(pivot_data.sum(axis=1), axis=0) * 100


plt.figure(figsize=(14, 8))
for column in pivot_percentage.columns:
    sns.lineplot(data=pivot_percentage, x=pivot_percentage.index, y=column, label=column, marker='o')

plt.title('Percentage of KitchenQual by DecadeBuilt')
plt.ylabel('Percentage (%)')
plt.xlabel('Decade Built')
plt.legend(title='KitchenQual')
plt.show()

ราคาบ้านกับปีที่สร้างมีความสัมพันธ์¶

ตอนแรกนึกว่าบ้านเก่าๆ อาจจะแพงเพราะ Classic ก็ได้ แต่หลักฐานก็คือราคาบ้านสัมพันธ์กับปีที่สร้างครับ

In [34]:
fig, ax = plt.subplots(figsize=(8, 6))


sns.regplot(x='YearBuilt', y='SalePrice', data=df, ax=ax, line_kws={"color": "red"})


ax.set_title('Year Built vs Sale Price')
ax.set_xlabel('Year Built')
ax.set_ylabel('Sale Price')


plt.show()

ย่าน North Ames ขายดีที่สุด¶

In [35]:
neighborhood_counts = df['Neighborhood'].value_counts()
neighborhood_counts.plot(kind='bar', figsize=(10,6))

plt.title('Number of Sales by Neighborhood')
plt.xlabel('Neighborhood')
plt.ylabel('Number of Sales')

plt.show()

บ้านในย่าน North Ames ได้รับความนิยมช่วงปี 1950-1960¶

ตอนแรกแอบเดาในใจว่าบ้านในย่านนี้ถูกสร้างช่วงปียุคหลังๆ นี่แหละ แต่พอลองเข้าไป Plot ดูจึงพบว่าไม่ใช่ เทรนด์การสร้างบ้านในย่านนี้ลดลงอย่างเห็นได้ชัด

In [36]:
df['DecadeBuilt'] = (10 * (df['YearBuilt'] // 10)).astype(str) + 's'
In [37]:
nam_filter = df[df['Neighborhood'] == 'NAmes'].copy()

decade_counts = nam_filter.groupby('DecadeBuilt').size()

decade_counts.plot(kind='line', marker='o', figsize=(10,6))
plt.title("Trend of Houses Built in 'NAmes' Neighborhood Over Decades")
plt.ylabel("Number of Houses")
plt.xlabel("Decade Built")
plt.grid(True)
plt.show()
In [38]:
grouped_df = df.groupby(['Neighborhood', 'DecadeBuilt']).size().reset_index(name='HouseCount')

# ใช้ FacetGrid ในการ Plot หลายภาพพร้อมกัน
g = sns.FacetGrid(grouped_df, col="Neighborhood", col_wrap=5, height=4, margin_titles=True, sharey=False)
g.map_dataframe(sns.lineplot, x="DecadeBuilt", y="HouseCount", marker="o")


g.set_titles(col_template="{col_name}")
g.set_axis_labels("Decade Built", "Number of Houses Built")
g.set_xticklabels(rotation=45)

g.tight_layout()

plt.show()

ลองทำ Scatter Plot สวยๆ ดูครับ นึกว่าจะดี ดูไม่รู้เรื่องซะงั้น

In [39]:
plt.figure(figsize=(15, 10))
sns.scatterplot(data=df, x="YearBuilt", y="SalePrice", hue="Neighborhood", palette="tab20", edgecolor=None, alpha=0.7)

plt.title("Sale Price by Year Built and Neighborhood")
plt.xlabel("Year Built")
plt.ylabel("Sale Price")
plt.xticks(rotation=45)
plt.legend(title="Neighborhood", bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()

plt.show()

ลองหาราคาเฉลี่ยกับจำนวนขายในแต่ละย่านเพื่อดูความนิยม¶

In [40]:
neighborhood_data = df.groupby('Neighborhood')['SalePrice'].agg(['count', 'mean'])


neighborhood_data.sort_values('count', ascending=False, inplace=True)


neighborhood_data.reset_index(inplace=True)


fig, ax1 = plt.subplots(figsize=(10,6))


sns.barplot(x='Neighborhood', y='count', data=neighborhood_data, ax=ax1, palette="Blues_d")
ax1.set_ylabel('Number of Sales')


ax2 = ax1.twinx()


sns.lineplot(x='Neighborhood', y='mean', data=neighborhood_data, ax=ax2, color='r', sort=False)
ax2.set_ylabel('Average Sale Price')


ax1.set_xticklabels(ax1.get_xticklabels(), rotation=90)

plt.title('Number of Sales and Average Sale Price by Neighborhood')
plt.tight_layout()
plt.show()

ยิ่งบ้านยิ่งใหม่ ยิ่งต้องมีโรงจอดรถ ?¶

ตอนแรกที่ขะตอบคำถามนี้ ผมทำ Scatter Plot ออกมาเพื่อจะดูจำนวนจุดว่ามีการสร้างโรงจอดเยอะจริงไหม แต่ลืมนึกไปว่าพอเศรษฐกิจโตขึ้น การสร้างบ้านควรจะเยอะขึ้นไปด้วย

ผมคิดว่าการทำ Scatter Plot ด้านบนอาจจะบอกไม่หมด เราอาจจะเห็นว่าพื้นที่โรงรถเยอะขึ้นก็จริง แต่อาจจะลืมนึกไปว่าบ้านที่มีพื้นที่โรงรถอาจจะเป็นส่วนน้อยเทียบกับบ้านที่สร้างในปีเดียวกันหรือไม่

มากไปกว่านั้น ไม่แน่ว่าระบบขนส่งสารธารณะอาจจะดีจนบ้านไม่จำเป็นต้องมีที่จอดก็ได้ ผมเลยลองหาสัดส่วนบ้านที่ไม่มีที่จอดลดในแต่ละ Decade ว่ามีแนวโน้มเพิ่มขึ้นหรือลดลงกันแน่

In [41]:
plt.figure(figsize=(12, 7))
sns.regplot(x=df['YearBuilt'], y=df['GarageArea'], scatter_kws={'s': 20, 'alpha': 0.5}, line_kws={'color': 'red'})
plt.title('Trend of GarageArea by YearBuilt')
plt.xlabel('Year Built')
plt.ylabel('Garage Area')
plt.tight_layout()
plt.show()
In [42]:
df['HasGarage'] = df['GarageArea'] > 0


df['DecadeBuilt'] = (10 * (df['YearBuilt'] // 10)).astype(str) + 's'
garage_decade_group = df.groupby('DecadeBuilt')['HasGarage'].apply(lambda x: 100 * (1 - x.mean()))


plt.figure(figsize=(12, 7))
garage_decade_group.sort_index().plot(kind='line', marker='o', color='skyblue')
plt.title('Percentage of Houses Without a Garage by Decade')
plt.xlabel('Decade Built')
plt.ylabel('Percentage Without Garage (%)')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.show()

ย่านไหน ใครอยู่ ?¶

เนื่องจากทำเลเป็นอะไรที่มีผลต่อราคาบ้านอย่างยิ่ง เราจะมาทำการศึกษากันต่อว่ากลุ่มผู้ซื้อบ้านแต่ละกลุ่ม อาศัยอยู่ที่ทำเลไหนกันบ้าง

โดยเราจะทำการแบ่งกลุ่มผู้ซื้อบ้านจากราคาขายเป็น 4 กลุ่มได้แก่

  1. Low Price : กลุ่มราคา 25% ต่ำสุด
  2. Medium Price : กลุ่มราคาระหว่าง 25%-75%

  3. High Price : กลุ่มราคาระหว่าง 75% ขึ้นไปจนถึง 95%

  4. Luxury : บ้านกลุ่มที่แพงที่สุด 5% แรก

การแบ่งกลุ่มครั้งนี้เพื่อจะทำความเข้าใจในย่านต่างๆ ว่าคนมีฐานะอาศัยอยู่โซนไหน คนฐานะกลางๆ อาศัยอยู่บริเวณไหนเป็นต้น

โดยจะ Visualized ข้อมูลด้วย Nightingale Chart (เพราะลองทำ Grouped Bar Chart แล้วไม่ Work ครับ ดูไม่รู้เรื่องเลย)

In [43]:
import numpy as np

cut_bins = [0,
            df['SalePrice'].quantile(0.25),
            df['SalePrice'].quantile(0.75),
            df['SalePrice'].quantile(0.95),
            df['SalePrice'].max()]

cut_labels = ['Low Price', 'Medium Price', 'High Price', 'Luxury']
df['PriceCategory'] = pd.cut(df['SalePrice'], bins=cut_bins, labels=cut_labels, include_lowest=True)


ct = pd.crosstab(df['Neighborhood'], df['PriceCategory'])
#บิดกราฟให้เป็น r-thetha co-ordinate
theta = np.linspace(0.0, 2 * np.pi, len(ct), endpoint=False)
colors = ['red', 'green', 'blue', 'purple']

for column, color in zip(ct.columns, colors):
    fig, ax = plt.subplots(figsize=(12, 12), subplot_kw={'projection': 'polar'})
    values = ct[column].values
    ax.bar(theta, values, alpha=0.6, label=column, color=color)
    ax.set_title(f"Distribution of {column} by Neighborhood")
    ax.set_xticks(theta)
    ax.set_xticklabels(ct.index, size=10)
    ax.set_theta_zero_location('N')
    ax.set_theta_direction(-1)  # ให้มันหมุนตามเข็ม
    plt.show()

จากข้อมูล เราพอจะเห็นภาพมากขึ้นแล้วว่าคนซื้อบ้านฐานะต่างๆ อาศัยอยู่บริเวณไหนกันบ้าง

กลุ่ม Low Price : เน้นอาศัยอยู่ย่าน Old Town,Edwars,Sawyer

Medium Price : เน้นอาศัยอยู่ย่าน Names,Mitchell,NorthPark Villa

High Price : เน้นอาศัยอยู่ย่าน College Creek, Crawford, Clear Creek

Luxury : เน้นอาศัยอยู่ย่าน Northridge,Northridge Heights, Stone Brook

บ้านชั้นเดียว ดูเหมือนจะได้รับความนิยมสูงสุด¶

เราลอง Plot Heatmap โดย Plot ย่านเรียงตามราคา เพื่อดูว่าบ้านที่แพงสุดไปถึงถูกสุดมีลักษณะการสร้างอย่างไร

จากข้อมูลก็พบว่าบ้านชั้นเดียว (One-Story) ดูจะเป็นอะไรที่ได้รับความนิยมมากไม่ว่าจะย่านไหน

แต่จุดที่น่าสังเกตุคือในย่านที่ราคาบ้านเฉลี่ยๆ ถูกๆ จะเริ่มเห็นบ้านแบบ 1.5Fin (บ้านที่มีชั้นสองเป็นสัดส่วนครึ่งหนึ่งของฐานชั้นแรก) ให้เห็นบ้าง

1.5-Storey-House-Diagram-768x606.png

และที่สำคัญคือบ้านชั้นเดียว ไม่ได้แปลว่าถูกนะครับ ทำออกมาราคาเฉลี่ยๆ ก็ไม่ได้แย่เลย

In [44]:
house_styles_count = df['HouseStyle'].value_counts().sort_values(ascending=False)
avg_price_per_style = df.groupby('HouseStyle')['SalePrice'].mean()[house_styles_count.index]


plt.figure(figsize=(14, 6))
ax = house_styles_count.plot(kind='bar', color='skyblue', position=0, width=0.4, align='center')


ax2 = ax.twinx()
avg_price_per_style.plot(kind='line', marker='o', ax=ax2, color='red', linewidth=2, label='Average Sale Price')


ax.set_title('Number of Houses Sold & Average Sale Price by HouseStyle')
ax.set_xlabel('HouseStyle')
ax.set_ylabel('Number of Houses Sold')
ax2.set_ylabel('Average Sale Price ($)')
ax.set_xticklabels(house_styles_count.index, rotation=45)
ax.legend(loc='upper left')
ax2.legend(loc='upper right')

plt.tight_layout()
plt.show()

Trend การทำบ้านชั้นเดียวเริ่มยุค 50s¶

จากข้อมูลปีที่สร้างบ้านกับลักษณะบ้านจะเห็นว่าสมัยก่อนบ้านแบบ 1.5Story กับบ้านสองชั้นได้รับความนิยม แต่พอมายุคหลังๆ เทรนด์เปลี่ยน คนหันมาชอบบ้านชั้นเดียวมากขึ้น และแน่นอนว่า North Ames เป็นที่ที่นำเทรนด์สร้างบ้านชั้นเดียว สอดคล้องกับด้านบนที่บอกว่าบ้านย่าน North Amnes สร้างในยุค 50s-60s แบบรัวๆ

In [45]:
grouped_housestyle = df.groupby(['DecadeBuilt', 'HouseStyle']).size().reset_index(name='Count')


pivot_housestyle = grouped_housestyle.pivot(index='DecadeBuilt', columns='HouseStyle', values='Count').fillna(0)


housestyle_percentage = pivot_housestyle.divide(pivot_housestyle.sum(axis=1), axis=0) * 100


ax = housestyle_percentage.plot(kind='area', figsize=(12, 7), alpha=0.5)
plt.title('Percentage of Each HouseStyle Built Over Decades')
plt.ylabel('Percentage')
plt.xlabel('Decade Built')
plt.legend(title='HouseStyle')
plt.tight_layout()
plt.show()
In [46]:
average_prices = df.groupby('Neighborhood')['SalePrice'].mean().sort_values(ascending=False)


contingency_table = pd.crosstab(df['Neighborhood'], df['HouseStyle'], values=df['SalePrice'], aggfunc='count').reindex(average_prices.index).fillna(0)


plt.figure(figsize=(14, 10))
sns.heatmap(contingency_table, annot=True, cmap='viridis', fmt='g')

plt.title('Relationship between HouseStyle and Neighborhood (sorted by Average SalePrice)')
plt.xlabel('HouseStyle')
plt.ylabel('Neighborhood')
plt.tight_layout()
plt.show()
In [47]:
sales_counts = df.groupby(['Neighborhood', 'YrSold']).size().reset_index(name='Count')


sales_pivot = sales_counts.pivot('Neighborhood', 'YrSold', 'Count').fillna(0)


sorted_neighborhoods = sales_pivot.sum(axis=1).sort_values(ascending=False).index
sales_pivot = sales_pivot.reindex(sorted_neighborhoods)


plt.figure(figsize=(12, 10))
sns.heatmap(sales_pivot, annot=True, fmt=".0f", cmap='viridis', cbar_kws={'label': 'Number of Houses Sold'})
plt.title('Number of Houses Sold by Year and Neighborhood')
plt.xlabel('Year Sold')
plt.ylabel('Neighborhood')

plt.show()

บ้านส่วนใหญ่เรียกร้องของ Condition แค่ Normal¶

ทีนี้ลองมาทำ Heatmap เพื่อดูความสัมพันธ์ระหว่าง Condition1 กับ Neighborhood โดยตั้งสมมุติฐานว่าบ้านคนรวยต้องขอเงื่อนไขอะไรแปลกๆ แน่

แต่ปรากฏว่าไม่ใช่แบบนั้น บ้านส่วนใหญ่ขอแค่ Normal Condition

In [48]:
contingency_table = pd.crosstab(df['Neighborhood'], df['Condition1'])


plt.figure(figsize=(14, 10))
sns.heatmap(contingency_table, annot=True, cmap='YlGnBu', fmt='g')

plt.title('Relationship between Condition1 and Neighborhood')
plt.xlabel('Condition1')
plt.ylabel('Neighborhood')
plt.tight_layout()
plt.show()
In [49]:
df['YearsBeforeSale'] = df['YrSold'] - df['YearBuilt']

average_prices = df.groupby('YearsBeforeSale')['SalePrice'].mean()

plt.figure(figsize=(10, 6))


sns.histplot(df['YearsBeforeSale'], kde=True, color='lightblue')


ax2 = plt.gca().twinx()


plt.title('Distribution of Years from Build to Sale & Average Sale Price')
plt.xlabel('Years Before Sale')
plt.ylabel('Count')



plt.tight_layout()
plt.show()

นานแค่ไหนกว่าบ้านจะต้องมีการ Remodel ?¶

อีกคำถามที่น่าสนใจในมุมของคนซื้อบ้านคืออายุบ้านประมาณไหนที่ควรจะทำการ Renovate ? วิธีการวิเคราะห์ข้อมูลตรงนี้ขอเลือกใช้การพล็อต Histogram เพื่อดูความถี่ของจำนวนปีที่มีการ Renovate

In [50]:
# ไม่เอาปีที่ Remodel เท่ากับปีที่สร้าง

remodeled_df = df[df['YearRemodAdd'] != df['YearBuilt']]


remodeled_df['YearsBeforeRemodel'] = remodeled_df['YearRemodAdd'] - remodeled_df['YearBuilt']


plt.figure(figsize=(10, 6))
sns.histplot(remodeled_df['YearsBeforeRemodel'], kde=True, bins=30)
plt.title('Distribution of Years from Build to Remodel')
plt.xlabel('Years Before Remodel')
plt.ylabel('Count')
plt.tight_layout()
plt.show()

จากภาพจะเห็นความแปลกอย่างหนึ่งคือในช่วงบ้านจะถูกปรับ Model ในช่วง 1 ปีหลังการสร้าง จากน้นจะเริ่มมีความถี่เยอะขึ้นจนมาอยู่ในช่วง 30 ปี ถึงจะทำการ Remodel อีกที

คำถามที่ Data Analyst ควรสืบค้นต่อไปจากข้อมูลชุดนี้คือ มันเกิดอะไรขึ้นในปีที่ 1 ทำไมคนถึงเลือกที่จะ Renovate ช่วงนี้บ่อยกันแน่ ? แต่ด้วยความที่ข้อมูลไม่เพียงพอจึงจะขอข้ามประเด็นนี้ไปก่อน

ภาวะเศรษฐกิจทำราคาบ้านลดลง¶

ในช่วงปี 2008-2009 สหรัฐได้เกิดวิกฤต SubPrime ขึ้น เราลองมาดูกันดีกว่าว่าในช่วงวิกฤตนั้น เราจะได้บ้านราคาถูกหรือไม่

  • เราจะได้บ้านราคาถูกหรือไม่
  • ​แล้วปริมาณการสร้างบ้านในช่วงวิกฤตนั้นลดลงหรือเพิ่มขึ้น ?
In [51]:
grouped_sales = df.groupby(['YrSold', 'PriceCategory']).size().reset_index(name='Number of Houses Sold')

plt.figure(figsize=(12, 7))
sns.lineplot(data=grouped_sales, x='YrSold', y='Number of Houses Sold', hue='PriceCategory', marker='o')
plt.title('Trend of Houses Sold Over the Years by Price Category')
plt.xlabel('Year Sold')
plt.ylabel('Number of Houses Sold')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.legend(title='Price Category')

plt.show()

คำถามของคำตอบแรกคือหลังปี 2009 แนวโน้มการสร้างบนลดลงอย่างเห็นได้ชัด โดยเฉพาะกลุ่มบ้านราคา Medium Price แต่จุดที่หน้าแปลกใจคือจำนวนการสร้างของอีกสามกลุ่มกลับเร่งตัวขึ้น

In [52]:
grouped_sales_avg = df.groupby(['YrSold', 'PriceCategory'])['SalePrice'].mean().reset_index()


grouped_sales_avg['Percentage Change'] = grouped_sales_avg.groupby('PriceCategory')['SalePrice'].pct_change() * 100

plt.figure(figsize=(12, 7))
sns.lineplot(data=grouped_sales_avg, x='YrSold', y='Percentage Change', hue='PriceCategory', marker='o')
plt.title('Percentage Change in Average Sale Price Over the Years by Price Category')
plt.xlabel('Year Sold')
plt.ylabel('Percentage Change in Average Sale Price')
plt.axhline(0, color='grey', linestyle='--')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.legend(title='Price Category')

plt.show()

ในส่วนของราคาบ้าน จากภาพด้านบนเราพอจะเห็นว่าในช่วงวิกฤต(ปี 2008) ราคาบ้านกลุ่ม Luxury ปรับตัวลงเกือบ 15% ตามมาด้วยบ้านราคาถูก ที่ปรับตัวลงไป -5% กว่าๆ ทั้งนี้สามารถสรุปได้ว่าถ้าเราอยากได้สินทรัพย์ชิ้นใหญ่ๆ ในราคาถูก ควรรอวิกฤต ในทางตรงกันข้าม บ้านสำหรับอยู่อาศัยทั่วไป ถึงแม้จะเกิดวิกฤต ราคาก็ไม่ได้ปรับตัวลงมากนัก

บ้านราคาถูก มักจะอยู่ในบริเวณที่มีประชากรปานกลาง¶

ตอนนี้ก่อนทำตั้งธงในใจไปก่อนว่าบ้านราคาถูกๆไปถึงกลางๆ ควรที่จะอยู่ในบริเวณที่ประชากรเยอะๆ (Residental High Density)

แต่พอได้ลองเล่นกับข้อมูลดูพบว่าบ้านราคากลางๆ ค่อนไปทางสูง อยู่ในโซน Residental Low Density)

แต่อันที่ไม่ต้องสืบเลยคือบ้านกลางน้ำ (Floating Village Residential) แพงสุด

จากการหาข้อมูลเพิ่มเติมพบว่าส่วนหนึ่งที่ทำให้ยังเป็น Medium Density ได้อยู่เพราะความหนาแน่นประชากรของสหรัฐต่ำ

โดยอ้างอิงจากข้อมูลของปี 2022 พบว่าความหนาแน่นของประชากรคือ

  • สหรัฐฯ 33 คน/ตารางเมตร
  • ไทย 140 คน/ตารางเมตร
In [53]:
grouped_mean = df.groupby('MSZoning')['SalePrice'].mean().sort_values(ascending=False).reset_index()

plt.figure(figsize=(10, 6))
sns.barplot(x='MSZoning', y='SalePrice', data=grouped_mean, palette='viridis')
plt.title('Average SalePrice by MSZoning')
plt.xlabel('MSZoning')
plt.ylabel('Average SalePrice')
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()
In [54]:
average_prices = df.groupby('Neighborhood')['SalePrice'].mean().sort_values(ascending=False)


sorted_neighborhoods = average_prices.index.tolist()


df['Neighborhood'] = pd.Categorical(df['Neighborhood'], categories=sorted_neighborhoods, ordered=True)


cross_matrix = pd.crosstab(df['Neighborhood'], df['MSZoning'])

plt.figure(figsize=(12, 10))
sns.heatmap(cross_matrix, annot=True, cmap='viridis', cbar=True, fmt="d")
plt.title('MSZoning by Neighborhood')
plt.xlabel('MSZoning')
plt.ylabel('Neighborhood')
plt.tight_layout()
plt.show()

บ้านขายถูกๆ ถ้าวางเงินดาวน์ต่ำๆ¶

อันนี้เคยเข้าใจว่าวางเงินดาวน์เยอะๆ แล้วบ้านจะถูกลง ปรากฏว่าไม่ใช่ บ้านขายถูกทางวางดาวน์น้อยๆ แต่ติดสัญญาไว้

ทีนี้ถ้าเห็นข้อมูลแบบนี้มันอาจจะสรุปโดยตรงไม่ได้ ขอเดาต่อว่ามันอาจจะเกิดจากคนมีเงินน้อย แล้วซื้อบ้านราคาถูกก็ได้ เลย Plot Heatmap ซ้ำอีกรอบเทียบเพื่อความชัวร์

In [55]:
plt.figure(figsize=(12, 7))


sns.barplot(data=df, x='SaleType', y='SalePrice', estimator=np.mean, ci=None, order=df.groupby('SaleType')['SalePrice'].mean().sort_values(ascending=False).index)

plt.title('Average SalePrice by SaleType')
plt.xlabel('SaleType')
plt.ylabel('Average SalePrice')
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()

จาก Normal Sale ไปเป็นซื้อก่อนบ้านเสร็จ¶

ข้อนี้เกิดจากความสงสัยว่าเมื่อระยะเวลาผ่านไป พฤติกรรมของคนขายบ้านเปลี่ยนไปมากน้อยขนาดไหน

ทำไมเรื่องนี้ถึงสำคัญ

เพราะบางทีบริษัทอสังหาริมทรัพย์ก็จัดโปรขายบ้านเพื่อเอาเงินสดมาก่อน โดยไม่ได้ดูคุณภาพของลูกค้า และแน่นอนว่ามันอาจจะส่งผลไปยังวันที่ลูกหนี้ไม่มีปัญญาจ่าย และเป็นสาเหตุของ NPA ในอนาคตได้

โดยจากข้อมูล เราเอาวิธีการขายมาทำเป็น Percent แล้วดูแนวโน้มว่าเป็นอย่างไรผ่าน Line-Chart

โดยข้อสรุปคือแนวโน้มการขายบ้านที่สร้างสำเร็จในช่วง 20 ปีให้หลังเน้นไปที่การตีราคาและขายก่อนที่บ้านจะสร้างเสร็จด้วยซ้ำ

ในขณะที่บ้านที่สร้างก่อนหน้าจะนิยมขายแบบปกติ

เลยอาจจะเดาได้ว่าลักษณะของผู้อยู่อาศัยเปลี่ยน พฤติกรรมผู้บริโภคอาจเปลี่ยนตาม

In [56]:
grouped = df.groupby(['DecadeBuilt', 'SaleCondition']).size().reset_index(name='Count')


pivot_df = grouped.pivot(index='DecadeBuilt', columns='SaleCondition', values='Count').fillna(0)

#ทำเป็น Percent
percentage_df = pivot_df.divide(pivot_df.sum(axis=1), axis=0) * 100

grouped = df.groupby(['DecadeBuilt', 'SaleCondition']).size().reset_index(name='Count')




ax = percentage_df.plot(kind='line', marker='o', figsize=(10, 7))
plt.title('Percentage of SaleCondition by DecadeBuilt')
plt.ylabel('Percentage')
plt.xlabel('Decade Built')
plt.legend(title='SaleCondition')
plt.show()

ถ้าอยากได้บ้านราคาถูก ต้องเสนอซื้อแบบ Abnormal และ Adjoining Land Purchase¶

ทีนี้ลองเอาค่าเฉลี่ยของการขายบ้านแต่ละกลุ่มมาเทียบกันดีกว่า Sale Condition แบบไหนจะทำให้ได้ราคาบ้านดีที่สุด

ซึ่งหลักฐานมันก็ชี้ชัดแล้วว่าการขายในภาวะไม่ปกติ ทั้งการขายแบบ Short Sale การแลกบ้าน ทำให้ได้ราคาดี

แต่ที่น่าแปลกคือการขายที่ดินที่ติดกัน กลับได้ราคาถูกทั้งถ้าเป็นย่านดีๆ มันควรราคาแพง อาจจะต้องลง Detail ต่อไป

แต่ส่วนตัวมองว่ามันอาจจะยังสรุปไม่ได้เพราะชุดข้อมูลน้อยเกินไป

In [57]:
# First, let's aggregate the data by SaleType
sale_type_data = df.groupby('SaleCondition')['SalePrice'].agg(['count', 'mean'])

# Sort the data by sales count
sale_type_data.sort_values('count', ascending=False, inplace=True)

# Reset the index for ease of plotting
sale_type_data.reset_index(inplace=True)

# Create a figure and axis
fig, ax1 = plt.subplots(figsize=(10,6))

# Bar plot for the count of sales per SaleType
sns.barplot(x='SaleCondition', y='count', data=sale_type_data, ax=ax1, palette="Blues_d")
ax1.set_ylabel('Number of Sales')
ax1.set_xlabel('Sale Type')

# Create a second y-axis for the average sale price
ax2 = ax1.twinx()

# Line plot for the average sale price per SaleType
sns.lineplot(x='SaleCondition', y='mean', data=sale_type_data, ax=ax2, color='r', sort=False)
ax2.set_ylabel('Average Sale Price')

# Rotate x-axis labels for better readability
ax1.set_xticklabels(ax1.get_xticklabels(), rotation=90)

# Set the title for the plot
plt.title('Number of Sales and Average Sale Price by SaleCondition')
plt.tight_layout()
plt.show()

มีบ้านแค่ 0.5% เท่านั้นที่มีสระว่ายน้ำ¶

อันนี้พอดีเกิดความสงสัยส่วนตัวว่าตลอดเวลาที่ผ่านมา บ้านที่มีสระว่ายน้ำเทียบกับไม่มีเยอะหรือไม่ เลยลองทำดู พบว่าจริงๆแล้วมีบ้านแค่ 0.5% เท่านั้นที่มีสระว่ายน้ำ

In [58]:
import matplotlib.pyplot as plt


pool = (df['PoolArea'] > 0).sum()
no_pool = (df['PoolArea'] == 0).sum()


labels = 'With Pool', 'Without Pool'
sizes = [pool, no_pool]
colors = ['gold', 'lightskyblue']
explode = (0.1, 0)


plt.figure(figsize=(10, 6))
plt.pie(sizes, explode=explode, labels=labels, colors=colors,
autopct='%1.1f%%', shadow=True, startangle=140)

plt.axis('equal')
plt.title('Distribution of Houses With and Without a Pool')
plt.show()

สรุป¶

จากที่ลองเล่นกับข้อมูลมา Insight ที่พอจะหาได้คือ

  • ราคาบ้านสัมพันธ์กับคุณภาพของบ้าน ถ้าคะแนนคุณภาพเยอะราคาจะสูง
  • ปีที่เศรษฐกิจไม่ดีมีโอกาสได้บ้านราคาถูกมากที่สุด
  • เทรนด์การสร้างบ้านเปลี่ยนจากสองชั้นเป็นชั้นเดียว
  • วิธีการเสนอซื้อแบบ Abnormal ได้ประโยชน์สูงสุด เพราะจะกดราคาได้
  • บ้านยุคใหม่มีโรงจอดรถแทบทั้งหมด สมมุติว่าต้องยึดบ้าน ควรเลือกบ้านที่มีโรงจอดดีกว่า เพราะเป็น Feature ที่สำคัญ

คำถามที่ควรถามต่อ

  • อะไรเป็นเกณฑ์สำหรับคะแนนสูงต่ำของบ้านแต่ละหลัง ?
  • ทำเลแต่ละทำเล มีจุดเด่นอย่างไร ? (ผมลองเอาชื่อทำเลไป Search ใน Google แล้วพบว่ามันเป็นชื่อทำเลในแต่ละรัฐ ซึ่งต้องมาลง Detail อีกว่าแต่ละรัฐมันมีอะไรเด่น มีเขตเศรษฐกิจอะไรหรือไม่)

สิ่งที่พอทำต่อได้กับ Data ชุดนี้

  • ทำ Machine Learning Model เพื่อ Predict ราคาบ้านที่ควรจะเป็น

ข้อเสนอแนะ

  • Dataset มีข้อมูลบ้านย่าน North Ames มากเกินไป
In [59]:
!jupyter nbconvert --execute --to html '/content/sample_data/EDA_House_Price_with_Python (2).ipynb'
[NbConvertApp] WARNING | pattern '/content/sample_data/EDA_House_Price_with_Python (2).ipynb' matched no files
This application is used to convert notebook files (*.ipynb)
        to various other formats.

        WARNING: THE COMMANDLINE INTERFACE MAY CHANGE IN FUTURE RELEASES.

Options
=======
The options below are convenience aliases to configurable class-options,
as listed in the "Equivalent to" description-line of the aliases.
To see all configurable class-options for some <cmd>, use:
    <cmd> --help-all

--debug
    set log level to logging.DEBUG (maximize logging output)
    Equivalent to: [--Application.log_level=10]
--show-config
    Show the application's configuration (human-readable format)
    Equivalent to: [--Application.show_config=True]
--show-config-json
    Show the application's configuration (json format)
    Equivalent to: [--Application.show_config_json=True]
--generate-config
    generate default config file
    Equivalent to: [--JupyterApp.generate_config=True]
-y
    Answer yes to any questions instead of prompting.
    Equivalent to: [--JupyterApp.answer_yes=True]
--execute
    Execute the notebook prior to export.
    Equivalent to: [--ExecutePreprocessor.enabled=True]
--allow-errors
    Continue notebook execution even if one of the cells throws an error and include the error message in the cell output (the default behaviour is to abort conversion). This flag is only relevant if '--execute' was specified, too.
    Equivalent to: [--ExecutePreprocessor.allow_errors=True]
--stdin
    read a single notebook file from stdin. Write the resulting notebook with default basename 'notebook.*'
    Equivalent to: [--NbConvertApp.from_stdin=True]
--stdout
    Write notebook output to stdout instead of files.
    Equivalent to: [--NbConvertApp.writer_class=StdoutWriter]
--inplace
    Run nbconvert in place, overwriting the existing notebook (only
            relevant when converting to notebook format)
    Equivalent to: [--NbConvertApp.use_output_suffix=False --NbConvertApp.export_format=notebook --FilesWriter.build_directory=]
--clear-output
    Clear output of current file and save in place,
            overwriting the existing notebook.
    Equivalent to: [--NbConvertApp.use_output_suffix=False --NbConvertApp.export_format=notebook --FilesWriter.build_directory= --ClearOutputPreprocessor.enabled=True]
--no-prompt
    Exclude input and output prompts from converted document.
    Equivalent to: [--TemplateExporter.exclude_input_prompt=True --TemplateExporter.exclude_output_prompt=True]
--no-input
    Exclude input cells and output prompts from converted document.
            This mode is ideal for generating code-free reports.
    Equivalent to: [--TemplateExporter.exclude_output_prompt=True --TemplateExporter.exclude_input=True --TemplateExporter.exclude_input_prompt=True]
--allow-chromium-download
    Whether to allow downloading chromium if no suitable version is found on the system.
    Equivalent to: [--WebPDFExporter.allow_chromium_download=True]
--disable-chromium-sandbox
    Disable chromium security sandbox when converting to PDF..
    Equivalent to: [--WebPDFExporter.disable_sandbox=True]
--show-input
    Shows code input. This flag is only useful for dejavu users.
    Equivalent to: [--TemplateExporter.exclude_input=False]
--embed-images
    Embed the images as base64 dataurls in the output. This flag is only useful for the HTML/WebPDF/Slides exports.
    Equivalent to: [--HTMLExporter.embed_images=True]
--sanitize-html
    Whether the HTML in Markdown cells and cell outputs should be sanitized..
    Equivalent to: [--HTMLExporter.sanitize_html=True]
--log-level=<Enum>
    Set the log level by value or name.
    Choices: any of [0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL']
    Default: 30
    Equivalent to: [--Application.log_level]
--config=<Unicode>
    Full path of a config file.
    Default: ''
    Equivalent to: [--JupyterApp.config_file]
--to=<Unicode>
    The export format to be used, either one of the built-in formats
            ['asciidoc', 'custom', 'html', 'latex', 'markdown', 'notebook', 'pdf', 'python', 'rst', 'script', 'slides', 'webpdf']
            or a dotted object name that represents the import path for an
            ``Exporter`` class
    Default: ''
    Equivalent to: [--NbConvertApp.export_format]
--template=<Unicode>
    Name of the template to use
    Default: ''
    Equivalent to: [--TemplateExporter.template_name]
--template-file=<Unicode>
    Name of the template file to use
    Default: None
    Equivalent to: [--TemplateExporter.template_file]
--theme=<Unicode>
    Template specific theme(e.g. the name of a JupyterLab CSS theme distributed
    as prebuilt extension for the lab template)
    Default: 'light'
    Equivalent to: [--HTMLExporter.theme]
--sanitize_html=<Bool>
    Whether the HTML in Markdown cells and cell outputs should be sanitized.This
    should be set to True by nbviewer or similar tools.
    Default: False
    Equivalent to: [--HTMLExporter.sanitize_html]
--writer=<DottedObjectName>
    Writer class used to write the
                                        results of the conversion
    Default: 'FilesWriter'
    Equivalent to: [--NbConvertApp.writer_class]
--post=<DottedOrNone>
    PostProcessor class used to write the
                                        results of the conversion
    Default: ''
    Equivalent to: [--NbConvertApp.postprocessor_class]
--output=<Unicode>
    overwrite base name use for output files.
                can only be used when converting one notebook at a time.
    Default: ''
    Equivalent to: [--NbConvertApp.output_base]
--output-dir=<Unicode>
    Directory to write output(s) to. Defaults
                                  to output to the directory of each notebook. To recover
                                  previous default behaviour (outputting to the current
                                  working directory) use . as the flag value.
    Default: ''
    Equivalent to: [--FilesWriter.build_directory]
--reveal-prefix=<Unicode>
    The URL prefix for reveal.js (version 3.x).
            This defaults to the reveal CDN, but can be any url pointing to a copy
            of reveal.js.
            For speaker notes to work, this must be a relative path to a local
            copy of reveal.js: e.g., "reveal.js".
            If a relative path is given, it must be a subdirectory of the
            current directory (from which the server is run).
            See the usage documentation
            (https://nbconvert.readthedocs.io/en/latest/usage.html#reveal-js-html-slideshow)
            for more details.
    Default: ''
    Equivalent to: [--SlidesExporter.reveal_url_prefix]
--nbformat=<Enum>
    The nbformat version to write.
            Use this to downgrade notebooks.
    Choices: any of [1, 2, 3, 4]
    Default: 4
    Equivalent to: [--NotebookExporter.nbformat_version]

Examples
--------

    The simplest way to use nbconvert is

            > jupyter nbconvert mynotebook.ipynb --to html

            Options include ['asciidoc', 'custom', 'html', 'latex', 'markdown', 'notebook', 'pdf', 'python', 'rst', 'script', 'slides', 'webpdf'].

            > jupyter nbconvert --to latex mynotebook.ipynb

            Both HTML and LaTeX support multiple output templates. LaTeX includes
            'base', 'article' and 'report'.  HTML includes 'basic', 'lab' and
            'classic'. You can specify the flavor of the format used.

            > jupyter nbconvert --to html --template lab mynotebook.ipynb

            You can also pipe the output to stdout, rather than a file

            > jupyter nbconvert mynotebook.ipynb --stdout

            PDF is generated via latex

            > jupyter nbconvert mynotebook.ipynb --to pdf

            You can get (and serve) a Reveal.js-powered slideshow

            > jupyter nbconvert myslides.ipynb --to slides --post serve

            Multiple notebooks can be given at the command line in a couple of
            different ways:

            > jupyter nbconvert notebook*.ipynb
            > jupyter nbconvert notebook1.ipynb notebook2.ipynb

            or you can specify the notebooks list in a config file, containing::

                c.NbConvertApp.notebooks = ["my_notebook.ipynb"]

            > jupyter nbconvert --config mycfg.py

To see all available configurables, use `--help-all`.